Python性能优化这事,说起来挺有意思。很多开发者对Python的第一印象就是“慢”,但真到线上出问题的时候,又不知道该从哪儿下手。我见过不少人一上来就甩锅给Python解释器,结果用cProfile一跑,发现是一段蹩脚的双层循环在作怪;也有反过来的,脚本看着挺正常,处理器占用却直接打满,最后靠py-spy才定位到问题。这篇文章就是想把整条路径串起来——从你本机的基准测速开始,到生产环境的线上热点诊断,把每个环节的工具、方法和判断逻辑都摊开讲清楚。不管你是刚把Python环境搭好的新手,还是已经被线上性能问题折磨过几轮的开发者,这套思路应该都能直接用上。
1. 优化前的思路梳理:到底什么才算性能瓶颈
1.1 性能优化的三个层次
先别急着拿工具到处测,我们需要先建立一个全局视角。Python性能问题通常分三个层面:架构层、代码层、基础设施层。架构层指的是你的系统设计,比如是不是选错了存储方案,是不是把N+1查询写进了主流程,这部分问题用再强的单机性能优化也救不回来。代码层就是我们日常最常碰到的,循环写得烂、数据结构选错、重复计算太多。基础设施层则是GC(垃圾回收)触发频繁、GIL(全局解释器锁)竞争、I/O等待过长这类问题,看起来像是“Python天生慢”,但其实是运行时机制导致的,有对应的解法。
很多教程一上来就教各种奇技淫巧,这是有问题的。我自己踩过最大的坑就是在一个Web接口上花了两天做微优化,改用缓存后才发现,真正的瓶颈是上游服务响应慢,读一次耗时300毫秒,而本机计算根本不到1毫秒。所以在做任何优化之前,先用一句话回答自己:这个瓶颈是“算得慢”还是“等得久”。算得慢就该看CPU占用、是不是算法太差;等得久就该看I/O、网络、锁竞争。
1.2 为什么必须“先测量再优化”
Python界有一句老话:“如果你没测量过,就不要猜瓶颈在哪里。”这句话听起来像废话,但违反它的人真不少。人类对程序的直觉偏差非常大,尤其是Python这么抽象的语言,真实的性能热点往往藏在让你意外的角落。
举个例子,你可能会觉得列表推导式肯定比普通for循环快,这个直觉大多数时候是对的,但也要看具体操作。你可能会觉得某个函数耗时多是因为里面有复杂计算,但实际分析后你会发现,大部分时间花在了重复的属性访问和函数调用开销上。测量就是在用数据代替猜测,把“我觉得这里慢”变成“数据证明这里慢”。
等到工具给出结果之后,也不必每个数据都紧张。任何程序都有热点集中在少数代码上的规律,也叫二八定律:80%的运行时间集中在20%的代码里。优化的任务就是找到那20%,把力气花在刀刃上,而不是把时间浪费在那些只在启动时运行一次的冷门代码上。
2. 基础测速方法论:把数据拿到手再说
2.1 不要再用time.time做基准测试
我见过很多人在代码里这么测速:
import time start = time.time() # 要测试的代码 result = some_function() end = time.time() print(f"耗时: {end - start} 秒")这段代码在小规模测试里勉强能用,但有两个问题。一是time.time调用的是系统墙上时钟(wall clock),它会受到系统时间调整的影响——比如NTP同步改时间,测出来的结果会变成负数。二是单次执行太容易受干扰,你的电脑可能在后台跑了个更新,风扇转起来,CPU降频,单次结果就会产生很大波动。
更好的选择是time.perf_counter。它专门用来测量短时间间隔,用系统提供的最高精度时钟实现,不受系统时间调整影响。下面是正确的基础测速写法:
import time start = time.perf_counter() result = some_function() end = time.perf_counter() print(f"耗时: {end - start:.6f} 秒")但即便用perf_counter,单次执行依然不够。正确的做法是重复多次,取中位数或者最小值。中位数能反映典型性能,最小值能反映在没有外部干扰的情况下能达到的最佳表现。平均值则容易被极值带偏,反而失去参考意义。
2.2 用timeit模块做严谨的微基准测试
如果你希望结果更可靠,或者你的测试对象是像list.append这种微小的操作,用timeit模块才是正路。timeit会自动帮你处理重复次数、垃圾回收干扰等细节,还能用命令行一键跑,非常方便。
python -m timeit -n 10000 -r 5 "','.join([str(i) for i in range(100)])"这行命令的意思是:把这段代码执行10000次,重复5轮,每轮取总耗时,最后报告每轮的平均值。timeit的好处是它会自动选择合适的重复次数,也可以手动指定,这样测试结果就比较稳定了。
在脚本内部,也可以这样使用timeit:
import timeit code = """ result = [i ** 2 for i in range(1000)] """ time_taken = timeit.timeit(stmt=code, number=10000) print(f"平均耗时: {time_taken / 10000:.6f} 秒")关于基准测试,有一条经验值得记住:微基准测试的结果只能作为方向参考,不能等同于真实场景下的性能。因为它测的是单一操作在理想状态下的表现,而真实代码里变量、内存布局、上下文切换都会改变结果。所以timeit适合用来做方案A和方案B的对比,不适合用来估算线上QPS。
2.3 设计基准测试时的常见干扰因素
就算用了timeit,如果测试代码写得不好,结果照样不可信。最常见的干扰因素有三个:
一是测试代码和被测代码之间的交互。比如你测一个有缓存功能的函数,第二次调用直接命中缓存,测出来“性能提升”了,但这不是真实情况。解决方法是每次测试前重置状态,或者用不同的输入数据。
二是垃圾回收的干扰。timeit默认会临时关闭GC,这其实是为了测试纯净度,但真实运行时GC是开着的。如果你测的是大量创建对象的内存敏感代码,建议在测试时保留GC:
import gc import timeit time_taken = timeit.timeit(stmt=code, number=10000, globals=globals())三是系统负载波动。我一般会在测试前关闭浏览器里的视频播放、同步工具等吃资源的后台程序,确保测出来的数据是当前环境下可复现的。
3. 代码层面的性能优化实操:从数据结构到写法细节
3.1 数据结构选型决定量级差异
Python内置的数据结构各有擅长的场景,选错类型的代价往往是数量级的差异。举个例子,判断一个元素是否存在于一组数据中,用list的in操作符,时间复杂度是O(n),数据量到万级以上就会明显变慢;而用set或dict的in操作,时间复杂度是O(1),几乎不随数据量变化。
你可能会觉得这个是基础中的基础,但我在实际项目里真的见过有人用list存了一万条订单号,每次请求进来都做一次in判断。当时QPS不高,没出问题,后来流量上来,接口直接被打崩了。排查的时候用cProfile一看,光这个in操作就占了40%的CPU时间。
快速对照表如下,方便你直接参考:
| 操作场景 | 推荐结构 | 不推荐结构 | 原因 |
|---|---|---|---|
| 频繁查找/模糊匹配 | set / dict | list | 哈希查找O(1) vs 线性扫描O(n) |
| 频繁在头部插入/删除 | collections.deque | list | list的insert(0)是O(n),deque是O(1) |
| 需要有序且频繁增删 | bisect + list | sorted list暴力插入 | 二分查位置O(log n),插入O(n)但常数小 |
| 键值对动态映射 | dict | list连续判断 | dict按key哈希查找,list需要遍历 |
| 记录最近N个元素 | collections.deque(maxlen=N) | list + 手动裁剪 | deque自动管理边界,裁剪不产生新列表 |
别小看这些“老生常谈”,数据结构选对,往往比后面所有微优化加起来都管用。
3.2 循环和函数调用中的隐藏开销
Python的函数调用和循环都有固定开销,这些开销虽然单次很小,但在热路径上会被放大。我给出几个直接能落地的技巧:
第一,尽量使用局部变量。Python读取局部变量比读取全局变量快得多,因为局部变量存在栈帧里,直接用下标访问;而全局变量需要两次字典查找。如果你有一个全局常量在循环里被反复使用,把它赋值给局部变量:
# 慢 GLOBAL_LIMIT = 10000 def slow_func(lst): result = [] for item in lst: if item < GLOBAL_LIMIT: result.append(item) return result # 快 def fast_func(lst): limit = GLOBAL_LIMIT result = [] append = result.append for item in lst: if item < limit: append(item) return result注意这里还有两个细节:把result.append赋值给局部变量append,避免每次调用时都做属性查找;把GLOBAL_LIMIT赋值给limit,避免全局查找。别小看这两个改动,在一个百万次循环里,它们能带来20%到30%的提升。
第二,能用内置方法解决的就别手写循环。比如要对一个列表求和,用sum(lst)比手写for循环累加快一个数量级,因为sum的循环在C层执行。类似的还有max、min、sorted、map、filter、itertools模块里的各种函数。这些内置函数不是在Python层逐条执行字节码,所以快很多。
第三,生成器表达式不一定总比列表推导式快。列表推导式会一次性创建完整的列表,生成器表达式是惰性求值的,内存占用低,但如果你只需要遍历一次,生成器占优势;如果后面还要多次遍历,列表更好。当需要把结果传给len、sum这类函数时,可以直接传生成器表达式的对象,减少一次列表内存分配:
# 不用先造列表再求和 total = sum(i * i for i in range(10_000_000))3.3 字符串拼接的三种姿势
字符串是Python里最容易被低估的性能陷阱。字符串是不可变对象,每次拼接都会创建一个新字符串。用+拼接大量字符串时,会产生大量中间对象,不仅慢还吃内存。
推荐的姿势是''.join(iterable)。它的原理是预先计算好所有元素总长度,一次性分配内存,然后填充,避免中间对象。实测下来,拼接一万个字符串,join比+快将近一个数量级。
另一个场景是格式化输出,f-string在Python 3.6以后性能已经不错,但如果是在循环里构造日志字符串,建议先判断日志等级是否开启,否则字符串格式化的开销白白消耗CPU。很多日志框架都内置了lazy formatting,如果直接用,也能避免不必要的格式化。
顺便说一句,如果你在循环里这样写代码:
# 慢 query = "" for item in items: query += f"'{item}',"快速改成:
# 快 query = ",".join(f"'{item}'" for item in items)这个优化在数据量几百条的时候看不出来,几万条时差距就很明显了。
3.4 用C扩展和向量化补齐短板
有一部分计算确实是纯Python无法承受的,如果你的场景是数值计算、矩阵运算、图像处理,可以考虑用NumPy、Pandas这类C扩展库来替代纯Python循环。它们的底层是C语言实现,而且很多操作做了向量化优化,能利用CPU的SIMD指令。
举个直观例子,对一百万元素分别做平方求和,纯Python循环大概需要上百毫秒,NumPy一行代码只需要几毫秒,差距是几十倍。这种量级的差异不是技巧能弥补的,需要的是选对工具。
不过引入NumPy也有成本,首先是库体量大,如果项目本身很小,只为了一段计算引入NumPy有点重;其次是循环次数太少的情况下,NumPy的调用开销可能反而高于纯Python,一般建议在数据规模超过一万以后再考虑用NumPy。
4. 初识cProfile:定位代码级瓶颈的利器
4.1 cProfile的基础使用方式
如果说上面的优化技巧是“主动预防”,那么cProfile就是“事后取证”的工具。cProfile是Python标准库里的性能分析器,它能记录每个函数的调用次数、总耗时、自身耗时,并把结果排序输出。用它定位瓶颈,比靠肉眼扫代码高效得多。
最基础的使用方式是在命令行直接跑一段脚本:
python -m cProfile -s cumulative my_script.py-s cumulative的意思是按累计耗时排序,这样你能一眼看到从入口函数开始,哪条调用链花的累计时间最多。如果按tottime排序,则能看到每个函数自身执行花了多少时间,不包含它调用的子函数。这两个视图各有用途:cumulative帮你找调用链,tottime帮你找具体的重函数。
也可以直接在代码中嵌入Profiler,尤其是当你只想分析程序的一部分时:
import cProfile import pstats profiler = cProfile.Profile() profiler.enable() # 你的核心代码 run_business_logic() profiler.disable() stats = pstats.Stats(profiler) stats.sort_stats('cumulative') stats.print_stats(20)print_stats(20)表示只显示前20条记录,避免被大量无关函数刷屏。生产环境如果要临时抓性能数据,可以输出到文件,再用工具分析:
python -m cProfile -o output.prof my_script.py python -m pstats output.prof4.2 读懂cProfile输出的一行信息
cProfile的输出对新手来说有点劝退,一屏下来全是函数名和数字。其实关键就四个字段:
- ncalls:调用次数。如果显示的是
12345/12345这种格式,可能和递归调用相关,斜杠前是原始调用次数,斜杠后是去重后的次数。 - tottime:函数自身执行总耗时(不含子调用)。
- percall:tottime除以调用次数,单次调用的自身耗时。
- cumtime:函数累计耗时(含所有子调用)。
通过对比tottime和cumtime,你能判断一个函数是自身重还是调用重。如果tottime很高,说明函数内部的计算逻辑有问题;如果tottime很低但cumtime很高,说明它调用的下游函数是瓶颈。
我一般先看cumtime排序的前几行,找到自己的业务函数;再看tottime排序,找到真正烧CPU的原语操作。两相对比,瓶颈就清楚了。
4.3 cProfile的使用注意事项
cProfile并不是没有代价的,它会显著拖慢程序运行速度,一般会让总体耗时增加50%到100%。所以在生产环境直接全面开启cProfile是有风险的,更合理的做法是:
- 在测试环境复现问题后开启cProfile,找出热点。
- 如果必须在生产环境采集数据,用采样式分析器(后面会讲到)。
- 脚本型任务可以直接用cProfile跑,Web服务不建议全面开启。
另一个容易被忽略的点是:cProfile默认不会跟踪C扩展库内部的函数调用,但它会显示对C扩展库的调用总时间。如果你发现某个第三方库的cumtime特别高,可能是库本身的计算量,也可能是你调用它的姿势不对,比如循环里重复创建对象。
5. 进阶分析工具箱:从行级到事件级
5.1 用line_profiler做逐行耗时分析
cProfile告诉你“哪个函数慢”,但函数内可能有几百行代码,你不一定知道具体是哪一行慢。这时就用line_profiler,它能够逐行统计每行代码的执行时间。
安装很简单:
pip install line_profiler使用时,在你关心性能的函数上加上装饰器:
@profile def process_data(data): result = [] for item in data: if item[2] == 'active': result.append(item) return result然后使用kernprof命令执行脚本:
kernprof -l my_script.py python -m line_profiler my_script.py.lprof输出会按行显示执行次数、单次耗时、总耗时和占用百分比。你能直观地看到瓶颈在哪一行,是if判断太慢,还是append操作太慢,还是循环本身的问题。
用line_profiler最典型的收获是:你可能会发现某行看似简单的代码其实特别慢。我有一次分析一个数据处理脚本,发现最慢的一行竟然是item[2] == 'active',原因是从数据库读出来的对象是ORM代理类,每次属性访问都会触发一次查询。这种问题光靠cProfile定位不到,因为cProfile只能看到函数级,而line_profiler直接暴露了行级细节。
5.2 用memory_profiler分析内存瓶颈
性能问题不只有CPU,内存也可能成为瓶颈。Python的内存管理对大部分场景是透明的,但如果你处理的是大文件、大列表,或者循环里不断创建临时对象,内存占用就有失控风险。
memory_profiler可以逐行显示内存占用变化。用法和line_profiler类似:
pip install memory_profiler@profile def load_data(): data = [] with open('big_file.txt', 'r') as f: for line in f: data.append(line) return data执行时用:
python -m memory_profiler my_script.py它会显示每行代码执行前后的内存增量。你能清楚地看到哪一行突然吃了几百兆内存,从而判断是读取方式的问题还是存数据结构的问题。
之前优化过一个日志处理任务,用memory_profiler一看,最大内存峰值出现在readlines()那一行,因为它一次性把整个文件读入内存。改成逐行读取之后,内存占用从500MB降到了30MB,性能反而提升了,因为省掉了大量内存分配和GC。
5.3 用Py-Spy对运行中的进程做采样分析
如果性能问题发生在生产环境的常驻进程上,你不能直接重启服务去加cProfile。这时候就需要采样式分析器。Py-Spy是这个领域的明星工具,它不需要修改代码,不需要重启进程,直接attach到运行中的Python进程上采集数据。
安装方式:
pip install py-spy然后查看进程ID,执行:
py-spy dump --pid 12345这会打印出进程当前所有线程的调用栈,相当于给运行中的程序拍了一张快照。如果你想持续采样并生成火焰图:
py-spy record -o profile.svg --pid 12345生成的SVG火焰图直观展示了CPU时间在各函数上的分布。用浏览器打开后,横轴是时间占比,纵轴是调用栈层数,越宽的函数越可能是瓶颈。火焰图特别适合分析“看起来没什么死循环、但CPU占用就是高”的问题。
Py-Spy的注意事项是:它在Linux上通常需要root权限或对进程至少有ptrace权限,在生产环境要注意权限配置。另外,采样只有统计意义,如果你的瓶颈是毫秒级的瞬态事件,采样可能捕捉不到,需要用其他手段补足。
6. 线上热点诊断实战:从症状到根因的完整路径
6.1 线上性能问题的典型症状
线上性能问题通常表现为三种症状,对应的排查方向完全不一样:
第一种,CPU使用率持续飙高,比如从20%涨到90%以上。这种情况多半是计算热点,可能是业务代码出现高复杂度循环、正则灾难性回溯、无限重试、GC风暴等。排查思路是用Py-Spy采样几份快照,看热点函数是什么。
第二种,接口延迟显著增加,但CPU占用不高。这种情况通常不是计算问题,而是等待问题——数据库查询慢、下游HTTP调用慢、锁竞争、网络丢包重传。排查思路是追踪调用链,看时间花在哪个外部依赖上。
第三种,内存持续增长,最终OOM。这种情况通常和内存泄漏、过大缓存、不变对象积累有关。排查思路是观察内存增长曲线,用tracemalloc或objgraph分析对象引用关系,找到谁占着内存不放。
以我自己亲身经历过的线上问题为例:某接口在低峰期耗时200ms,高峰期涨到5s,CPU占用却只有30%。刚开始团队以为是代码性能问题,各种优化都没效果。后来用分布式跟踪工具定位,发现高峰期Redis响应变慢,很多请求被阻塞在Redis的socket读取上。真正的问题不是Python代码,而是Redis实例的连接数和内存碎片问题。这个案例说明,线上诊断不能只盯着Python进程内部,也要看它依赖的外部系统。
6.2 线上诊断的“由外到内”三步法
遇到线上性能问题,我习惯按照“由外到内”的顺序排查,避免一开始就钻进代码细节里。
第一步,看外部指标。先看CPU、内存、I/O、网络带宽这些系统级指标,确认问题是不是出在资源耗尽上,比如机器负载过高,其他进程抢占了CPU。再看应用的请求量、错误率、延迟分位数,确认问题的触发条件和影响范围。
第二步,看应用内部热点。用Py-Spy进行采样,获取调用栈分布。如果是在Docker容器里,注意容器内没有专门的Python进程管理工具,你可以结合容器cgroup等指标辅助判断。如果采样结果显示热点集中在你自己的业务代码里,就看具体的函数;如果热点集中在内置库或第三方库,要看是不是调用姿势有问题。
第三步,看代码和依赖。根据前两步的结论,回到代码里检查具体的实现细节,是数据结构选型不对,还是出现了死循环,还是锁住的资源太多。这一步要和基础测速工具配合使用,在本地构造类似场景复现,验证修复方案。
6.3 用日志和APM工具做持续监控
单次诊断只能解决当下的问题,如果线上性能问题反复出现,就考虑引入持续监控手段。
日志层面,规范的日志格式和耗时记录是关键。每个API接口至少记录请求ID、处理总耗时、依赖调用耗时、状态码。有了这些数据,才能做耗时分布分析,判断是否出现性能劣化趋势。
APM层,像Django Silk(开发期)、SkyWalking、Jaeger这类工具可以提供请求级别的调用链追踪,展示SQL查询耗时、函数调用关系、错误异常。商业APM工具往往更方便,但自建方案也不复杂,关键是先让请求链路可视化。
有的同学可能觉得这些太重了,一个小项目搞什么APM。但我的经验是:哪怕只是一个简单的Flask应用,只要在入口装饰器里记录每个接口的耗时分布,存到日志里,就已经能解决大部分线上问题定位需求。工具的选择取决于你的项目规模,但“记录与度量”这个习惯一定要养成。
6.4 性能优化的验证与回归
修复完性能问题之后,不能直接上线就完事。需要做两件事:性能回归验证和容量影响评估。
性能回归验证是指在相同条件下,用修复前的压测数据和修复后的压测数据做对比,确认性能确实提升了,而且没有引入错误。压测工具可以选wrk、locust或JMeter,根据你的协议和并发模型来定。关键是控制变量:同样的机器、同样的数据量、同样的并发数,这样才能得出可信的对比结论。
容量影响评估是指确认新方案对部署资源的需求变化。比如你用上了缓存,内存占用从1GB涨到2GB,那线上机器是否足够?比如你用上了多进程,CPU核数是否够用?带着这些指标一起上线,就不至于刚修复一个瓶颈又制造一个新瓶颈。
7. 常见问题与排查技巧实录
7.1 常见问题速查表
这一节我把平时遇到的高频问题整理成表格,方便你直接对照排查:
| 现象 | 可能原因 | 定位工具 | 解决方案 |
|---|---|---|---|
| 单函数耗时高但自身代码很简洁 | 调用外部系统/数据库慢 | cProfile的cumtime、分布式链路追踪 | 加缓存、异步化、SQL加索引 |
| CPU飙高但代码看不出死循环 | 正则灾难性回溯 | Py-Spy取样看调用栈 | 改用更严谨的解析方式,限制回溯次数 |
| 内存持续增长最后崩溃 | 循环里累积大对象 | tracemalloc、objgraph | 改为逐条处理、释放引用、使用生成器 |
| 并发请求时接口突然极慢 | GIL竞争或锁竞争 | Python线程监控、缓存计数器 | 改用多进程、降低锁粒度、避免CPU密集任务用线程 |
| 用cProfile后程序慢到无法运行 | profiler开销过大 | Py-Spy、轻量抽样 | 将profile范围缩小,或用采样模式 |
| 代码优化后性能无变化 | 瓶颈根本不在本机代码 | 系统监控、链路追踪 | 排查外部依赖、网络、磁盘I/O |
| 微基准测试很快但线上很慢 | 测试场景和线上场景差异大 | 压测工具 | 造更接近线上数据的测试数据 |
7.2 独家优化经验:三个容易忽略的检查点
第一个检查点:临时对象的创建频率。Python里每次循环都可能创建大量临时对象,比如在循环里用f'{a}-{b}'拼接字符串,数量上来后会带来严重的GC压力。可以用gc.set_debug(gc.DEBUG_STATS)观察GC触发频率,如果有大量0代对象频繁产生,就考虑减少不必要临时对象。
第二个检查点:函数调用深度的累积效应。每个Python函数调用都有开销,如果业务逻辑被拆成几百个极小函数又互相嵌套调用,性能消耗也不容小觑。不是说函数拆分不好,而是在性能敏感的路径上适当合并热函数调用是有价值的。
第三个检查点:异常处理的代价。有些代码习惯用try-except做正常流程控制,比如判断一个元素是否在dict中,先直接取再捕获KeyError。当KeyError真的频繁发生时,异常处理的开销远高于一次if判断。改用if key in dict或者在.get()方法里设置默认值,性能会有明显改善。
7.3 从“遇到问题”到“建立优化意识”的成长路径
写完这一大堆工具和技巧,我想说的是,性能优化能力的本质不是会背命令、会用工具,而是形成一套自己的诊断思维闭环:观察症状,提出假设,用工具验证,修复,再回归。这个闭环每走一遍,你对Python运行机制的理解都会加深一层。
对于新手来说,建议从一个小脚本开始练习:写一段逻辑清晰但故意留了几个性能陷阱的代码,比如嵌套循环、重复的字符串拼接、用list做大量查找。先用timeit测出基线,再用cProfile跑一遍,找出热点,最后逐项优化。走完这个过程,你学到的东西比看十篇教程都多。
对于有经验的开发者,建议在项目里养成性能备忘的习惯:每次优化完一个点,记录下问题现象、定位工具、根因和修复代价。时间长了,你会发现自己看一眼代码就能猜出80%的瓶颈位置,剩下的20%交给工具去验证。
8. 我的几点亲身体会
最后分享几个我在实践中养成的习惯和体会。第一个体会是“先跑起来,再谈性能”。在项目早期过度优化往往是浪费时间的,只有等真实流量和真实数据出现了,你才能分清哪些性能问题值得解决、哪些解决完也不会对用户产生实际影响。业务需求和代码可维护性永远排在性能优化前面。
第二个体会是“工具链要趁早熟悉”。cProfile、line_profiler、py-spy这些工具,别等线上出问题的时候才第一次用。新项目搭起来之后,花半天时间跑一遍这些工具,看看有没有明显不合理的性能特征,比事后救火轻松得多。
第三个体会是“优化要留可测量的证据”。不管你优化了什么,先用压测工具记录基线,再记录优化后的结果。没有证据的优化只是感觉上的变快,有数据的优化才能让你在团队里说服别人,也能作为后续排障的参照。
补一个小技巧:做性能对比的时候,记得把测试环境的版本、依赖、CPU型号、数据规模都记录下来。我自己就遇到过换了台机器之后,优化效果从“提升30%”变成“反而变慢5%”,坑就坑在对比环境不一致。环境一致的前提下,同一份代码的性能差异才能真实反映你的优化效果,否则只是机器差异在作祟。
Python的性能优化从来不神秘,它是一套有方法论、有工具链、可验证的工程实践。