第4章:基准测试框架:timeit模块详解、cProfile性能剖析、自定义计时装饰器
聊到算法性能验证,很多人第一反应是「跑一下看看」。但怎么跑、跑几次、结果怎么解读,这里面的门道可不少。我见过不少同学拿time.time()一包,打印个差值就完事了。嗯,这种做法在小脚本里凑合能用,但真要拿来做性能基准测试,坑太多了。
这一章,我带你系统梳理一下Python里做基准测试的三种主流手段。说白了,就是让你知道什么时候该用哪个工具,怎么用才靠谱。
4.1 timeit模块:最精准的微基准测试
timeit是Python标准库自带的模块,专门用来测量小段代码的执行时间。它为什么比手动time.time()靠谱?因为它会自动帮你做三件事:
- 关闭垃圾回收:避免GC干扰计时结果
- 多次重复执行:取最小值或平均值,减少偶然误差
- 高精度计时:底层调用操作系统的高精度时钟
我个人习惯在写算法题或者优化热点函数时,直接用timeit来验证优化效果。举个例子:
import timeit
# 测试列表推导式 vs 普通for循环
setup_code = "nums = list(range(1000))"
list_comp = "[x*2 for x in nums]"
for_loop = """
result = []
for x in nums:
result.append(x*2)
"""
t1 = timeit.timeit(list_comp, setup=setup_code, number=10000)
t2 = timeit.timeit(for_loop, setup=setup_code, number=10000)
print(f"列表推导式: {t1:.4f}秒")
print(f"for循环: {t2:.4f}秒")
print(f"快了: {(t2-t1)/t1*100:.1f}%")
输出结果很直观。我在项目中遇到过好几次,明明觉得for循环可读性更好,但性能差距就是摆在那里。这时候,数据说话。
timeit的number参数别设太小。我一般设1000到10000,让总时间在0.2秒以上,这样计时误差占比就小了。
4.2 cProfile性能剖析:找到真正的瓶颈
timeit适合测小段代码,但如果你面对的是一个几百行的函数,甚至一个完整的模块,那就得请出cProfile了。
cProfile是Python内置的性能剖析器。它能告诉你:
- 每个函数被调用了多少次
- 每次调用花了多少时间(包括子函数)
- 累计时间、平均时间等等
说白了,它就是一张「性能账单」,让你一眼看出钱(时间)花在了哪里。
import cProfile
import pstats
def slow_function():
total = 0
for i in range(1000000):
total += i ** 2
return total
def fast_function():
return sum(i*i for i in range(1000000))
# 性能剖析
cProfile.run('slow_function()', 'profile_stats')
p = pstats.Stats('profile_stats')
p.sort_stats('cumtime').print_stats(10)
输出结果里,ncalls是调用次数,tottime是函数自身耗时,cumtime是包含子调用的总耗时。我一般先看cumtime,从最大的开始优化。
4.3 自定义计时装饰器:灵活又优雅
有时候,你不想改原有代码结构,又想给某个函数加上计时功能。这时候装饰器就派上用场了。我写了一个通用的计时装饰器,你可以直接拿去用:
import time
import functools
def timer(repeat=1):
"""计时装饰器,支持重复执行取平均"""
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
total_time = 0
for _ in range(repeat):
start = time.perf_counter()
result = func(*args, **kwargs)
end = time.perf_counter()
total_time += (end - start)
avg_time = total_time / repeat
print(f"[{func.__name__}] 平均耗时: {avg_time*1000:.2f}ms (重复{repeat}次)")
return result
return wrapper
return decorator
# 使用示例
@timer(repeat=5)
def compute_something(n):
return sum(i**0.5 for i in range(n))
compute_something(1000000)
这个装饰器有几个好处:
- 可重复:通过
repeat参数控制执行次数,取平均值更稳定 - 无侵入:加一行
@timer就行,不影响原有逻辑 - 可组合:可以和其他装饰器(比如缓存、日志)一起用
我在做算法竞赛训练时,经常用这个装饰器来快速对比不同实现的性能。你想想看,改一行代码就能看到优化效果,多方便。
4.4 三种工具的选择策略
最后,我整理了一个对比表格,方便你根据场景选择:
| 工具 | 适用场景 | 精度 | 开销 | 推荐用法 |
|---|---|---|---|---|
| timeit | 小段代码(<100行) | 高 | 低 | 验证单个算法或表达式的性能 |
| cProfile | 完整模块或函数调用链 | 中 | 中高 | 定位性能瓶颈,找出热点函数 |
| 自定义装饰器 | 特定函数的持续监控 | 中 | 低 | 开发调试、对比不同实现 |
我的建议是:先拿cProfile跑一遍,找到最耗时的函数。然后用timeit或装饰器去细粒度优化那个函数。这样既不会漏掉瓶颈,也不会在无关紧要的地方浪费时间。
嗯,这一章的内容就到这里。工具是死的,人是活的。多跑几次,多对比,慢慢你就能培养出「性能直觉」——看到一段代码,大概能猜到它慢在哪里。这种能力,光看书是学不来的,得动手。