Python技术迷

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

CPU 打满,代码没动几行,机器风扇先起飞了。 这种 Python 程序我见得不少:接口不慢,数据库也没炸,偏偏一个离线任务跑得像在蹬三轮。你去看代码,没什么高深逻辑,就是一堆 for、一堆 if,再夹点字符串处理。这个时候我一般先不急着“优化代码”,先怀疑一件事:你是不是还在老老实实用 CPython 硬扛。

很多人对 Python 性能的理解还停在一句废话上:Python 慢。 这句话对,也不对。慢的往往不是“业务逻辑”,而是你让解释器一条一条吃掉那些纯 Python 指令。尤其是这种代码:

defcalc_score(rows):
    total = 0
for row in rows:
        uid = row["uid"]
        text = row["text"]
ifnot uid ornot text:
continue

        s = 0
for ch in text:
            o = ord(ch)
if48 <= o <= 57:
                s += o * 3
elif65 <= o <= 90:
                s += o * 2
elif97 <= o <= 122:
                s += o
        total += s
return total

这类代码有个特点: 没怎么调数据库。 没怎么调网络。 全是解释器在那儿循环、判断、对象分配。

这种场景下,第一反应不是把 for 改成列表推导,不是先上多线程,更不是吭哧吭哧重写成 C。先拿 PyPy 跑一遍。

就这么直接:

# 原来这样跑
python job.py

# 现在换个解释器
pypy3 job.py

代码一行不改。 不少纯 Python 任务,速度就是肉眼可见地往上窜。

原因也不神秘。CPython 是解释执行,跑这种热循环时开销比较碎。PyPy 带 JIT,会把反复执行的热点路径编译掉。你代码写得越“土”,全是循环和分支,它反而越有机会提速。这个事第一次遇到时,很多人都不太信,觉得“换个解释器能有多大差别”。真跑一下,态度一般就老实了。

我平时会先用一个最笨但是很稳的办法确认是不是这类问题,先剖一下执行时间:

import cProfile
import pstats

from job import calc_score, load_rows

if __name__ == "__main__":
    rows = load_rows("/data/tmp/orders.log")
    cProfile.run("calc_score(rows)", "perf.out")

    p = pstats.Stats("perf.out")
    p.sort_stats("cumtime").print_stats(10)

如果 top10 里面全是你自己的 Python 函数,尤其是循环处理、文本清洗、规则匹配这种东西,那我就基本不会先去抠语法糖了。先换解释器,成本最低。

我自己写过一个日志清洗脚本,核心逻辑跟线上排障时临时搓的脚本差不多:

defclean_lines(lines):
    result = []
for line in lines:
if"trace_id="notin line:
continue

        parts = line.strip().split("|")
if len(parts) < 5:
continue

        cost = 0
for item in parts:
if item.startswith("cost="):
                value = item[5:]
if value.isdigit():
                    cost = int(value)
break

if cost > 200:
            result.append(line)
return result

这种东西在 CPython 下跑 3000 万行日志,慢得很诚实。换成 PyPy,常常不是快 20%,是直接一个数量级地缩。标题里说“50倍”,不是说所有项目都能到,别拿这个当保健品广告看。但在纯 Python 热循环、对象操作密集、长时间运行任务里,翻十几倍甚至更夸张,我真不意外。

不过丑话得放前面,别一激动就全项目切过去。PyPy 不是万能药,这几个场景我会先打个问号。

第一,重度依赖 C 扩展的,比如不少 NumPy、pandas、一些机器学习库,未必占便宜。 第二,程序特别短,启动就结束,JIT 还没热起来,收益不明显。 第三,线上环境如果绑了某些只支持 CPython 的库,别为了提速把运行时搞崩。

所以我的排查顺序一般是这样。

先确认瓶颈是不是 Python 自己在干活。 再挑一个离线任务、批处理脚本、ETL 作业做试点。 同一份代码,用 CPython 跑一遍,用 PyPy 跑一遍。 看耗时、看内存、看结果是否一致。

压根不用先重构。

可以直接写个最小对比脚本:

import time

from job import load_rows, calc_score

rows = load_rows("/data/tmp/orders.log")

begin = time.time()
score = calc_score(rows)
end = time.time()

print(f"score={score}, cost={end - begin:.3f}s")

然后分别执行:

python benchmark.py
pypy3 benchmark.py

别光看“快没快”,顺手把内存也盯一下。有些任务 PyPy 会吃掉更多内存,这个要提前知道,别在测试机上挺欢,到了容器里直接被 OOM 干掉。

还有个误区,我也顺手踩过。有人一看 Python 慢,立刻开始改成 multiprocessing,进程开一堆,结果机器上下文切换飞起,日志刷得像瀑布,最后并没有更快。因为根还在:单进程里那坨纯 Python 代码本来就跑得笨。解释器不换,优化动作很容易做成体力活。

所以这事最值钱的地方,不是“PyPy 很牛”,而是它提醒你一件事: 别一上来就改业务代码。 先看解释器。 先看热点是不是纯 Python。 先做最低成本验证。

很多性能问题,真不是算法多玄学,也不是架构多高级,就是默认选项一路跑到底,没人怀疑过而已。Python 还能这么跑,很多人确实没试过。等你试过一次,再回头看那些加班加出来的“微优化”,多少会有点嫌弃。