如何使用timeit模块
我该如何使用 timeit
来比较我自己写的函数,比如 "insertion_sort
" 和 "tim_sort
" 的性能呢?
15 个回答
我来告诉你一个秘密:使用 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
。
如果你想在交互式的Python环境中使用 timeit
,有两个很方便的选择:
使用 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
在普通的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]
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
要注意的是,每次执行语句时,都会新建一份未排序的数据副本。
另外,注意这个计时技巧:运行测量程序七次,只保留最好的时间——这样可以有效减少由于系统上其他进程运行而导致的测量误差。