timeit.timeit() 直接调用常不准,因其默认不关闭gc、仅运行100万次、字符串执行导致作用域失真;应改用timer类+repeat()取最小值,并通过setup和globals确保上下文一致。

timeit 是 Python 里最靠谱的小段代码计时工具,但它默认行为容易误导——比如不自动禁用 GC、不重复足够次数、不处理变量作用域,直接调 timeit.timeit() 得到的数字很可能偏小或抖动大。
为什么 timeit.timeit() 直接调用常不准?
默认只运行 100 万次(number=1000000),但对极快的代码(比如 x = 1 + 1)来说,CPU 调度、缓存预热、GC 干扰都会拉低结果可信度;更麻烦的是,传入的字符串代码在全局作用域执行,而实际业务中变量常在函数内定义,作用域差异会导致字节码不同、优化程度不同。
- 不关 GC:垃圾回收可能在某次迭代中触发,耗时突增,拉高整体平均值
- 没设
number:太小 → 统计噪声大;太大 → 可能溢出或耗时过长 - 字符串形式传代码:无法复用局部变量,也不能用
nonlocal/global声明
用 timeit.Timer 类 + repeat() 才算稳
绕开字符串限制,把待测逻辑封装成函数,再用 Timer 绑定该函数和 setup 逻辑(如导入、初始化),最后用 repeat(repeat=3, number=100000) 多轮跑——每轮独立计时,取最小值(排除系统干扰峰值)比平均值更反映真实性能。
-
repeat=3表示跑 3 轮,每轮执行number次 - 选最小值而非平均值:系统中断、缓存未命中等偶然开销只会抬高单轮耗时,最小值更接近“理想路径”
- setup 函数里做 import 和初始化:避免计入计时,且确保每次 repeat 都重置状态
import timeit <p>def test_func(): return sum(range(100))</p><h1>正确姿势:分离 setup 和 stmt</h1><p>timer = timeit.Timer(stmt=test_func, setup='from <strong>main</strong> import test_func') times = timer.repeat(repeat=3, number=100000) print(min(times)) # 输出单位是秒 </p>
测量函数内联 vs 调用开销时,别漏掉 globals 参数
如果想对比 math.sqrt(4) 和 4 ** 0.5,必须让它们在相同作用域下运行,否则 math 模块查找路径不同会影响结果。用 globals=locals() 把当前命名空间透传进去,比写字符串拼接安全得多。
- 字符串方式:
timeit.timeit('math.sqrt(4)', setup='import math')——math在 setup 里导入,但 stmt 字符串执行时仍要解析名字 - 函数方式:
timeit.timeit(lambda: math.sqrt(4), globals=locals())——math已在当前 scope,无查找开销 - 注意:
globals=locals()仅适用于测试代码在模块顶层或函数内,不能在类方法里直接用
微秒级代码要警惕 CPU 时间精度和时钟源
timeit 底层用 time.perf_counter(),在多数现代系统上精度达纳秒级,但实际分辨率取决于 OS 和硬件。Windows 上 perf_counter() 可能只有 15ms 分辨率(尤其旧版本),导致 number 太小时测出来全是 0.0。
- 遇到结果为 0.0 或极小(如
2.3e-07),先增大number到 10 万以上再看 - Linux/macOS 一般没问题;Windows 用户建议升级到 Python 3.7+,并确认系统开启了高精度定时器
- 不要用
time.clock()(已弃用)或time.time()替代 —— 它们受系统时间调整影响,不适合基准测试
真正难的不是调哪个函数,而是让被测代码处在和生产环境一致的上下文里:同样的作用域、同样的导入链、同样的 GC 状态。少一次 setup 错误,比多跑十轮 timeit 更接近真相。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











