第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,从最大的开始优化。

避坑指南:我曾经在线上环境不小心开了cProfile,结果性能反而下降了30%。记住,cProfile本身有开销,只适合在开发或测试环境用,千万别在生产环境长期开启。

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或装饰器去细粒度优化那个函数。这样既不会漏掉瓶颈,也不会在无关紧要的地方浪费时间。

核心要点:基准测试不是为了证明「我的代码快」,而是为了找到「哪里可以更快」。数据不会骗人,但前提是你得用对工具。
基准测试工具选择流程图 需要性能验证 代码规模? 小段代码 (<100行) 完整模块 (函数调用链) 推荐:timeit 或自定义装饰器 推荐:cProfile 性能剖析 定位瓶颈 → 针对性优化

嗯,这一章的内容就到这里。工具是死的,人是活的。多跑几次,多对比,慢慢你就能培养出「性能直觉」——看到一段代码,大概能猜到它慢在哪里。这种能力,光看书是学不来的,得动手。

可可精品资料库