最近接手一个项目,客户抱怨脚本执行越来越慢,尤其是数据量上去之后,原本几分钟能跑完的任务现在要半个多小时。查了一圈才发现,问题出在解释器上——用的是标准的 Python CPython 解释器,但没做任何性能调优。这种情况其实挺常见的,很多开发者只关注业务逻辑,忽略了运行环境本身的效率。
缓存字节码,减少重复解析
Python 每次运行脚本都会先编译成字节码(.pyc 文件),如果文件没变,其实没必要每次都重新编译。启用字节码缓存是最基础的操作。确保 __pycache__ 目录可写,并且在部署时保留已生成的 .pyc 文件,能省下不少启动时间。
python -B script.py # 禁止写入 .pyc(调试用)
python -O -m compileall . # 预编译所有模块
换用更快的解释器
CPython 是官方默认,但它有 GIL(全局解释器锁),多线程并行受限。换成 PyPy 就不一样了。PyPy 带 JIT 编译,对长时间运行的脚本特别友好。我们把一个数据清洗任务从 CPython 切到 PyPy,执行时间直接从 28 分钟降到 6 分钟。
当然,不是所有库都兼容 PyPy,比如某些用 C 扩展的包。上线前得先测一遍依赖项。
减少动态查找开销
Python 动态性很强,但也带来性能代价。比如在一个循环里频繁访问 sys.path 或 import 模块,就会拖慢速度。把需要的对象提到外面,或者用局部变量缓存,效果很明显。
<!-- 错误示范 -->
for i in range(100000):
import json
data = json.loads('{}')
<!-- 正确做法 -->
import json
for i in range(100000):
data = json.loads('{}')
用 C 扩展关键路径
有个统计计算模块一直卡顿,后来发现是纯 Python 实现的递归函数。改用 Cython 重写核心部分后,性能提升了近 15 倍。Cython 能把带类型注解的 Python 代码编译成 C,再封装成模块调用,对数值计算类任务特别合适。
# setup.py
from setuptools import setup
from Cython.Build import cythonize
setup(ext_modules = cythonize("_compute.pyx"))
避免频繁的内存分配
有个日志处理脚本,每条记录都拼字符串,结果内存暴涨,GC 频繁触发。改成用 ''.join() 批量处理,或者用 io.StringIO 缓冲输出,内存占用降了七成,速度也上来了。
还有就是尽量复用对象。比如用 collections.deque 替代 list 做队列操作,避免 insert(0, x) 这种高成本动作。
利用并发绕过 GIL
单进程跑不过去的时候,就得分。用 multiprocessing 把任务拆到多个子进程,每个都有独立的解释器实例,能真正利用多核。我们有个报表生成服务,原来是串行处理 50 个区域的数据,改成多进程后,总耗时从 40 分钟压到 9 分钟。
from multiprocessing import Pool
def process_region(region_id):
# 处理逻辑
return result
with Pool(8) as p:
results = p.map(process_region, region_ids)
监控热点,精准优化
别一上来就瞎猜瓶颈在哪。用 cProfile 跑一遍脚本,看看哪些函数耗时最多。
python -m cProfile -o profile.out script.py
python -m pstats profile.out
打开分析结果,一眼就能看到 top 几个耗时函数。有时候你会发现,拖慢速度的并不是算法本身,而是某个日志打印语句里用了 str(object) 导致大量反射调用。