如何使用timeit模块

469 投票
15 回答
455982 浏览
提问于 2025-04-17 06:48

我该如何使用 timeit 来比较我自己写的函数,比如 "insertion_sort" 和 "tim_sort" 的性能呢?

15 个回答

194

我来告诉你一个秘密:使用 timeit 的最佳方式是在命令行上。

在命令行中, timeit 会进行正确的统计分析:它会告诉你最短运行时间。这很好,因为所有的计时误差都是正的。所以最短的时间误差最小。因为计算机不可能计算得比它能计算的更快,所以没有负误差的情况!

所以,命令行界面:

%~> python -m timeit "1 + 2"
10000000 loops, best of 3: 0.0468 usec per loop

这很简单,对吧?

你可以设置一些东西:

%~> python -m timeit -s "x = range(10000)" "sum(x)"
1000 loops, best of 3: 543 usec per loop

这也很有用!

如果你想写多行代码,可以使用命令行的自动续行,或者使用单独的参数:

%~> python -m timeit -s "x = range(10000)" -s "y = range(100)" "sum(x)" "min(y)"
1000 loops, best of 3: 554 usec per loop

这会给你一个设置:

x = range(1000)
y = range(100)

并且计时:

sum(x)
min(y)

如果你想写更长的脚本,可能会想把 timeit 放到 Python 脚本里。我建议避免这样,因为在命令行上的分析和计时效果更好。相反,我通常会写 shell 脚本:

 SETUP="

 ... # lots of stuff

 "

 echo Minmod arr1
 python -m timeit -s "$SETUP" "Minmod(arr1)"

 echo pure_minmod arr1
 python -m timeit -s "$SETUP" "pure_minmod(arr1)"

 echo better_minmod arr1
 python -m timeit -s "$SETUP" "better_minmod(arr1)"

 ... etc

这可能会因为多次初始化而花费更多时间,但通常这不是大问题。


但是如果你 在你的模块里使用 timeit 呢?

简单的方法是这样做:

def function(...):
    ...

timeit.Timer(function).timeit(number=NUMBER)

这会给你运行指定次数的累计时间(而不是最小时间)。

为了得到好的分析,使用 .repeat 并取最小值:

min(timeit.Timer(function).repeat(repeat=REPEATS, number=NUMBER))

通常你应该把这个和 functools.partial 结合使用,而不是 lambda: ...,这样可以减少开销。因此你可以写成:

from functools import partial

def to_time(items):
    ...

test_items = [1, 2, 3] * 100
times = timeit.Timer(partial(to_time, test_items)).repeat(3, 1000)

# Divide by the number of repeats
time_taken = min(times) / 1000

你也可以这样做:

timeit.timeit("...", setup="from __main__ import ...", number=NUMBER)

这会给你一个更接近命令行界面的效果,但方式没那么酷。 "from __main__ import ..." 让你可以在 timeit 创建的虚拟环境中使用主模块的代码。

值得注意的是,这只是 Timer(...).timeit(...) 的一个方便封装,所以在计时上并不是特别好。我个人更喜欢使用 Timer(...).repeat(...),就像我上面展示的那样。


警告

使用 timeit 时有一些注意事项。

  • 开销没有被考虑进去。比如你想计时 x += 1,来看看加法需要多长时间:

    >>> python -m timeit -s "x = 0" "x += 1"
    10000000 loops, best of 3: 0.0476 usec per loop
    

    其实它并不是 0.0476 微秒。你只知道它 小于 这个值。所有的误差都是正的。

    所以要尽量找出 纯粹的 开销:

    >>> python -m timeit -s "x = 0" ""      
    100000000 loops, best of 3: 0.014 usec per loop
    

    仅仅是计时就有30%的开销!这会严重影响相对计时。但你真正关心的只是 加法 的计时;查找 x 的时间也需要算入开销:

    >>> python -m timeit -s "x = 0" "x"
    100000000 loops, best of 3: 0.0166 usec per loop
    

    差别不大,但确实存在。

  • 修改方法是危险的。

    >>> python -m timeit -s "x = [0]*100000" "while x: x.pop()"
    10000000 loops, best of 3: 0.0436 usec per loop
    

    但这 完全错误! x 在第一次迭代后是空列表。你需要重新初始化:

    >>> python -m timeit "x = [0]*100000" "while x: x.pop()"
    100 loops, best of 3: 9.79 msec per loop
    

    但这样会增加很多开销。要单独考虑这些开销。

    >>> python -m timeit "x = [0]*100000"                   
    1000 loops, best of 3: 261 usec per loop
    

    注意,这里减去开销是合理的,仅仅因为 开销是时间的一个小部分。

    对于你的例子,值得注意的是 插入排序Tim 排序 对于已经排序的列表有完全不寻常的计时行为。这意味着如果你想避免影响计时,你需要在排序之间使用 random.shuffle

348

如果你想在交互式的Python环境中使用 timeit,有两个很方便的选择:

  1. 使用 IPython 这个命令行工具。它有一个很方便的特殊功能 %timeit

    In [1]: def f(x):
       ...:     return x*x
       ...: 
    
    In [2]: %timeit for x in range(100): f(x)
    100000 loops, best of 3: 20.3 us per loop
    
  2. 在普通的Python解释器中,你可以通过在设置语句中从 __main__ 导入你之前定义的函数和其他名称来访问它们:

    >>> def f(x):
    ...     return x * x 
    ... 
    >>> import timeit
    >>> timeit.repeat("for x in range(100): f(x)", "from __main__ import f",
                      number=100000)
    [2.0640320777893066, 2.0876040458679199, 2.0520210266113281]
    
323

timeit的工作原理是先运行一次准备代码,然后重复执行一系列语句。所以,如果你想测试排序,得小心点,确保一次就地排序的结果不会影响下一次已经排序的数据(这样的话,Timsort就会表现得特别好,因为它在数据部分有序时效果最佳)。

下面是一个设置排序测试的例子:

>>> import timeit

>>> setup = '''
import random

random.seed('slartibartfast')
s = [random.random() for i in range(1000)]
timsort = list.sort
'''

>>> print(min(timeit.Timer('a=s[:]; timsort(a)', setup=setup).repeat(7, 1000)))
0.04485079200821929

要注意的是,每次执行语句时,都会新建一份未排序的数据副本。

另外,注意这个计时技巧:运行测量程序七次,只保留最好的时间——这样可以有效减少由于系统上其他进程运行而导致的测量误差。

撰写回答