ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Python还能这么跑?不用改代码,性能飙升50倍

Python还能这么跑?不用改代码,性能飙升50倍 还能这么跑不用改代码性能飙升50倍有段 脚本平时跑一批对账文件要二十多分钟。代码没改SQL 没改机器也没换只把启动命令从python check_bill.py换成pypy3 check_bill.py第二次跑完我第一反应不是高兴是不太信。这般性能提升实在是离谱至极, 离谱得我通常会率先怀疑三件事情: 其一, 是否数据未曾读取完备其二, 是不是逻辑有所提前其三, 会不会是缓存把结果给糊弄过去了。所以我先加了几行很土的日志import time import os started time.perf_counter rows 0 bad_rows 0 with open(bill_202406.log, r, encodingutf-8) as f: for line in f: rows 1 parts line.rstrip(\n).split(|) if len(parts) ! 6: bad_rows 1 continue user_id, order_no, amount, channel, status, sign parts # 模拟线上老脚本里的校验逻辑纯 Python 字符串和整数计算 seed 17 for ch in order_no channel amount: seed (seed * 131 ord(ch)) 0xFFFFFFFF if sign ! hex(seed)[2:]: bad_rows 1 cost time.perf_counter - started print({ pid: os.getpid, rows: rows, bad_rows: bad_rows, cost: round(cost, 3), })日志能对上{pid: 31872, rows: 3000000, bad_rows: 1847, cost: 1268.442}换 PyPy 后{pid: 31940, rows: 3000000, bad_rows: 1847, cost: 24.931}这地方就有意思了。代码未曾变动, 结果照样相同, 所耗费的时间, 从原本二十多分钟, 大幅下降到二十五秒上下, 大约降低了五十倍。这并非是, 忽然间就开窍了, 也不是说 PyPy 对于全部代码而言都这般厉害。它所适用的是一种极为特定的场景, 即存在大量纯粹的循环, 有关字符串的处理, 还有整数的计算, 逻辑会反复地执行, 并且代码路径比较稳定。我以前见过不少 慢脚本慢得很冤。比如这种defpick_risk_orders(lines): risky for line in lines: cols line.strip.split(,) if len(cols) 5: continue order_id cols[0] user_level cols[2] amount int(cols[3]) city cols[4] score 0 if amount 5000: score 40 if user_level in (new, guest): score 20 for c in city: score ord(c) % 7 if score 55: risky.append(order_id) return risky这种代码在 里跑慢不奇怪。它的每一行, 都在进行对于执行的逐个解析, 每次进入循环, 都需要去查找变量, 调用函数, 拆解字符串, 转换为整数, 一旦业务数量越来越多, 数据规模越来越大, CPU 表面上呈现出忙碌的状态, 可实际上, 大量的时间均耗费在了解释器自身所产生的开销之上。PyPy 不一样。它具备 JIT, 代码运行一会儿之后, 它察觉到某一 段循环格外热, 就会将这段逻辑编译成更趋近于机器能直接去执行的事物。热循环越显著, 收益也就越大。我这类脚本一般不先上来就改算法。先看三个东西。第一慢在哪里。python -m cProfile -s cumtime check_bill.py | head -30如果前面全是业务函数比如ncalls tottime cumtime function 3000000 411.23 913.54 parse_line 3000000 302.18 302.18 calc_sign 3000000 166.91 166.91 normalize_channel这种就值得拿 PyPy 试一下。第二依赖干不干净。PyPy 对于单纯的情况是很友善的, 然而要是你的项目当中存在大量的 C 扩展, 就像某些科学计算方面的库、图像相关的库、加密类的库, 那收益就未必如此了。并非是不能够运行, 而是需要先去进行验证。我一般先这么扫一眼python - PY import pkg_resources suspects for dist in pkg_resources.working_set: name dist.project_name.lower if name in {numpy, pandas, cryptography, pillow, lxml}: suspects.append(dist.project_name) print(native-like packages:, suspects) PY有这些包不代表 PyPy 没戏只是别拍脑袋上线。第三脚本是不是跑得够久。PyPy存在提前预热所需的成本, 对于一个在0.3秒就结束运行的小脚本而言, 倘若换成它, 或许反而会变得更慢, 因为在JIT还没能够来得及开展工作的时候, 程序就已经完成运行并结束了。所以我喜欢用这种方式做对比别只跑一次import subprocess import time cmds [ (cpython, [python, check_bill.py]), (pypy, [pypy3, check_bill.py]), ] for name, cmd in cmds: costs for i in range(3): t1 time.perf_counter subprocess.run(cmd, checkTrue) costs.append(round(time.perf_counter - t1, 3)) print(name, costs)看第二次、第三次更有意义。这里有个坑我得单独说一下。可别将“换 PyPy”视作性能优化的那种能解决一切问题的万能药。接口运行速度迟缓, 数据库运行速度迟缓, 网络传输速度迟缓, 文件 IO 操作进行得缓慢, 它可解决不了多少这类状况。比如这种代码defsync_user(conn, api): users conn.query(select id, mobile from user_pending limit 10000) for user in users: resp api.post(/user/check, json{mobile: user[mobile]}) if resp[ok]: conn.execute( update user_pending set checked1 where id%s, (user[id],) )这个慢多半不是 慢。第一眼, 我会先查看接口耗时, 再看数据库执行计划, 接着看连接池, 然后轮到批量提交, 不会首先去更换解释器。你将它丢给PyPy, 也就是把循环那微乎其微的开销节省掉, 然而真正占据大头的部分还在外面呢。但是, 要是属于这种离线清洗的情况, 还有规则匹配, 以及日志扫描, 再加上批量校验, 甚至是协议解析, PyPy极有可能会有惊喜出现。再贴一个更接近线上小工具的写法。defscan_nginx_error(path): stat {} with open(path, r, encodingutf-8, errorsignore) as f: for line in f: if upstream timed out notin line: continue api - pos line.find(GET ) if pos 0: pos line.find(POST ) if pos 0: left line.find( , pos 1) right line.find( , left 1) if left 0and right left: api line[left 1:right].split(?)[0] stat[api] stat.get(api, 0) 1 return sorted(stat.items, keylambda x: x[1], reverseTrue)[:20] if __name__ __main__: for api, cnt in scan_nginx_error(access.log): print(cnt, api)这种脚本 写着顺手但文件一到几十 GB就开始磨人。当然, 那你能够改成为awk, 可以借助Rust重新进行编写, 并且也能够运用Spark。然而不少情形下仅仅是进行临时的排查, 实在是没什么必要将事情弄得那般严重。首先去换 PyPy 运行一次, 这成本最为低廉。安装也不复杂。# Ubuntu sudo apt install pypy3 # macOS brew install pypy3跑之前看下版本pypy3 -V虚拟环境可以单独建别和原来的 环境混着来pypy3 -m venv .venv-pypy source .venv-pypy/bin/activate pip install -r requirements.txt然后启动命令改一下pypy3 check_bill.py严格说这已经算“换运行时”了不是玄学优化。代码依旧是那一份代码, 然而运行它的解释器发生了更换。这恰似同一条 SQL, 于不同的数据库引擎之下, 在不同的执行计划之中, 其表现或许全然就不是同一个样子了。我现在处理 慢脚本大概会按这个顺序来先确认结果一致。再用 看是不是纯 热循环。然后用 PyPy 跑一版。如果提升明显就继续压依赖兼容性、内存、启动耗时。如果提升不明显再回头改算法、批处理、并发、IO。不要一上来就重构。有不少运行速度迟缓的代码并非编写水准欠佳, 仅仅是运行的方式存在问题。特别是那些年代久远的脚本, 没有人敢于去改动, 哪怕改动一行都担心会对账目产生影响。在这样的时候, “不更改代码”反倒是最为稳妥的着手点。不过那句“性能飙升 50 倍”别乱贴到所有 项目上。适用于它的情况是, 处理CPU密集型任务, 执行单纯的循环操作, 运行执行时间较长, 所依赖之要素并不复杂。若命中了这几个条件, PyPy着实会把人吓得不轻。要是没命中, 那就别强行吹嘘。去运行一下, 日志便能道出实情。
返回列表