Python 脚本越跑越慢?用 cProfile 和 timeit 定位性能瓶颈
标题:《Python 脚本越跑越慢?用 cProfile 和 timeit 定位性能瓶颈》
写 Python 脚本时常遇到一种情况:数据量不大,程序却越跑越慢;换台机器还是一样,凭感觉优化改了半天,速度却没变化。性能问题最怕凭感觉下手——先测出时间到底花在哪儿,再动手改,方向才不会错。Python 自带的 cProfile 和 timeit 就是干这个的,不用装第三方库。
第一步:用 cProfile 看整体耗时分布
怀疑某个脚本慢,先别急着重构,用 cProfile 跑一遍看全貌:
python3 -m cProfile -s cumtime myscript.py
输出里重点看三列:ncalls 是调用次数,tottime 是函数自身耗时(不含子调用),cumtime 是包含子调用的总耗时。按 cumtime 排序,排在最前面的就是热点。如果某个函数被调用了几十万次,哪怕单次很快,累计起来也很可观——这时该想的是减少调用次数,而不是把函数体写得更快。
第二步:用 timeit 精确对比小段代码
定位到可疑的小段代码后,用 timeit 做精确对比,别用 time.time() 手掐表——后者受系统波动影响大:
python3 -m timeit -s "data = list(range(10000))" "sum(x * 2 for x in data)"
python3 -m timeit -s "data = list(range(10000))" "sum([x * 2 for x in data])"
timeit 默认多次运行取最优,能过滤掉偶发的系统抖动。两种写法跑下来,生成器和列表推导的差距一目了然。改动前后各测一次,数字变了才算优化有效,否则只是自我安慰。
第三步:先看算法,再谈微观优化
很多慢不是写法问题,而是算法问题。比如在循环里反复用列表做成员判断:
# 慢:每次 in 都要线性扫描
for x in items:
if x in big_list:
...
把 big_list 换成集合,判断从线性变成接近常数时间,数据量越大差距越明显:
lookup = set(big_list)
for x in items:
if x in lookup:
...
同样,字符串拼接别在循环里用 += 反复拼,大量拼接时先收集到列表再用 join 一次拼好。先修算法层面的数量级问题,再考虑循环展开、局部变量缓存这类微观技巧,收益顺序不能颠倒。
第四步:确认瓶颈在 CPU 还是在等 IO
还有一种常见误判:程序慢不是算得慢,而是在等——等网络响应、等磁盘读写、等数据库返回。cProfile 里如果 cumtime 大但 tottime 小,时间大概率花在等待上,这时优化计算没有意义,该做的是减少请求次数、加缓存、批量读写,或者把串行等待改成并发。分清 CPU 瓶颈和 IO 瓶颈,优化方向完全不同,测之前先问一句:时间到底是在算,还是在等?
最后提醒一句:优化要有度。先用 cProfile 找到真正的热点,改一处测一次,用 timeit 拿数字说话;没进热点的代码,再丑也先放着。性能优化的目标不是把代码写得花哨,而是把时间花在真正值得的地方。
- 点赞
- 收藏
- 关注作者
评论(0)