如何使用 timeit 模块

465

如何使用timeit来比较自己编写的函数(例如“insertion_sort”和“tim_sort”)的性能?

15个回答

345
如果你想在交互式Python会话中使用timeit,有两个方便的选项:
  1. Use the IPython shell. It features the convenient %timeit special function:

    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. In a standard Python interpreter, you can access functions and other names you defined earlier during the interactive session by importing them from __main__ in the setup statement:

    >>> 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

请注意,在每次迭代中,该系列语句会对未排序的数据进行一次新的复制。
此外,请注意运行测量套件七次并仅保留最佳时间的计时技术——这真的可以帮助减少由系统上运行的其他进程引起的测量失真。

8
是的,它包括列表复制(与排序本身相比非常快)。但如果不复制,第一次遍历会对列表进行排序,而剩下的遍历就不需要做任何工作。如果你想知道仅排序所需的时间,那么可以在有和没有“timsort(a)”情况下运行上述代码并计算时间差异 :-) - Raymond Hettinger
104
请使用min()函数而不是平均时间。这是我、Tim Peters和Guido van Rossum的建议。当缓存已加载并且系统没有忙于其他任务时,最快的时间代表算法可以执行的最佳性能。所有的计时都有噪音,最快的时间是最少有噪音的。很容易证明,最快的计时是最可重复的,因此在比较两种不同实现的计时时最有用。 - Raymond Hettinger
6
你需要对1000个输入进行“平均值”(实际上是总和,但含义相同)的计算,然后重复7次,取“最小值”。你需要对1000个输入进行平均值计算,因为你想要平均(而不是最佳情况)的算法复杂度。你需要最小值,正如你所说的原因。我认为我可以通过选择一个输入,运行算法7次,取最小值,然后重复1000次并取平均值来改进你的方法。但我没有意识到你的.repeat(7,1000)已经这样做了(使用相同的种子)! 所以在我看来,你的解决方案是完美的。 - max
6
我只能补充一点,你如何分配7000个执行次数的预算(例如.repeat(7, 1000).repeat(2, 3500).repeat(35, 200)相比)应该取决于系统负载误差与输入变化误差的比较。如果你的系统总是处于重负载状态,并且在长时间空闲状态下捕获到执行时间分布图左边有一个细长的尾巴,即使你不能超过7000次运行次数,你可能会发现.repeat(7000,1).repeat(7,1000)更有用。 - max
1
把已经在设置中的数组复制一份,创建一个迭代器it,然后计时'a=next(it); timsort(a)',这个方案怎么样? - Stefan Pochmann
显示剩余4条评论

190

我告诉你一个秘密:使用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

这也很有用!

如果你想要多行文本,可以使用 shell 的自动换行或使用单独的参数:

%~> 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)

如果你想要更长的脚本,你可能会想要在 Python 脚本内使用 timeit。但我建议避免这样做,因为在命令行上分析和计时会更好一些。相反,我倾向于创建 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


139

如果你想快速比较两个代码块/函数,可以这样做:

import timeit

start_time = timeit.default_timer()
func1()
print(timeit.default_timer() - start_time)

start_time = timeit.default_timer()
func2()
print(timeit.default_timer() - start_time)

只需使用time.perf_countertime.perf_counter_ns即可。 - user3064538

51
我发现使用timeit的最简单方法是从命令行开始。
给定的test.py文件:
def insertion_sort(): ...
def timsort(): ...

运行 `timeit` 的方法如下:
% python -m timeit -s 'import test' 'test.insertion_sort()'
% python -m timeit -s 'import test' 'test.timsort()'

喜欢它!在Windows命令提示符下,我不得不使用双引号来避免SyntaxError: unterminated string literal错误。是的,我来晚了。 - undefined

34

对我来说,这是最快的方法:

import timeit
def foo():
    print("here is my code to time...")


timeit.timeit(stmt=foo, number=1234567)

14

这个很棒:

  python -m timeit -c "$(cat file_name.py)"

1
Windows 的等价物是什么? - Shailen
5
如果脚本需要参数,你该如何传递参数? - Juuso Ohtonen
@Shailen 我发现 python -m timeit -s $(cat file_name.py) 可以在 PowerShell 中使用。如果在 $() 周围加上引号,PowerShell 似乎会将 file_name 中的所有内容输出到一行中,从而导致错误。 - thegoodhunter-9115

14
# Генерация целых чисел

def gen_prime(x):
    multiples = []
    results = []
    for i in range(2, x+1):
        if i not in multiples:
            results.append(i)
            for j in range(i*i, x+1, i):
                multiples.append(j)

    return results


import timeit

# Засекаем время

start_time = timeit.default_timer()
gen_prime(3000)
print(timeit.default_timer() - start_time)

# start_time = timeit.default_timer()
# gen_prime(1001)
# print(timeit.default_timer() - start_time)

11

只需将整个代码作为timeit的一个参数进行传递:

import timeit

print(timeit.timeit(

"""   
limit = 10000
prime_list = [i for i in range(2, limit+1)]

for prime in prime_list:
    for elem in range(prime*2, max(prime_list)+1, prime):
        if elem in prime_list:
            prime_list.remove(elem)
"""   
, number=10))

4

让我们在以下所有地方设置相同的字典并测试执行时间。

setup参数基本上是设置字典

Number用于运行代码1000000次。不是setup而是stmt

当你运行它时,可以看到索引比获取速度更快。可以多次运行以查看。

该代码基本上尝试获取字典中c的值。

import timeit

print('Getting value of C by index:', timeit.timeit(stmt="mydict['c']", setup="mydict={'a':5, 'b':6, 'c':7}", number=1000000))
print('Getting value of C by get:', timeit.timeit(stmt="mydict.get('c')", setup="mydict={'a':5, 'b':6, 'c':7}", number=1000000))

以下是我的结果,您的可能会有所不同。

按索引访问:0.20900007452246427

使用get方法访问:0.54841166886888


你正在使用哪个版本的Python? - Eduardo

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接