跳转到主要内容

timeit的强大多行替代方案

项目描述

GithubActions Appveyor Codecov Pypi Downloads ReadTheDocs

Timerit

Python内置timeit模块的强大多行替代方案。

文档发布在https://timerit.readthedocs.io/en/latest/,但本README和代码注释包含一个教程。

Github

https://github.com/Erotemic/timerit

Pypi

https://pypi.ac.cn/project/timerit

ReadTheDocs

https://timerit.readthedocs.io/en/latest/

描述

通过简单地缩进现有代码块,可以轻松地进行健壮的计时。无需将其重构为字符串表示形式或转换为单行。

安装

pip install timerit

交互式使用

timerit库为交互式使用提供了简洁的API

>>> import timerit
>>> for _ in timerit:
...     sum(range(100000))
Timed for: 288 loops, best of 5
    time per loop: best=616.740 µs, mean=668.933 ± 124.2 µs

与timeit比较

$ python -m timeit 'sum(range(100000))'
500 loops, best of 5: 721 usec per loop

默认情况下,任何在循环中的代码都将重复执行,直到至少200毫秒已过去。然后打印出计时结果。

以下每个数字的含义

  • “288 loops”:循环中的代码在达到时间限制之前运行了288次。

  • “5次最快”:在计算平均值和标准差时,只考虑每5次测量中的最快时间。这样做的原因是,如果后台有某个进程正在消耗资源,可能会导致出现较慢的时间,因此你通常只对最快的时间感兴趣。这个想法也已在timeit文档中描述。

  • “best=616.740 µs”:最快迭代运行所需的时间。如上所述,这通常是数据中最一致的数量,也是你应该关注的重点。

  • “mean=668.933 ± 124.2 µs”:5次最快迭代中的平均值和标准差。这个统计数据通常不如最快时间稳健或有用,但有时如果存在较大的方差,了解它也是有帮助的。

循环变量可以用作上下文管理器,以只对每次循环的一部分进行计时(例如,为了提高计时精度,或包含不被计时的设置阶段)

>>> for timer in timerit:
...     n = 100 * 1000
...     with timer:
...         sum(range(n))
Timed for: 318 loops, best of 5
    time per loop: best=616.673 µs, mean=617.545 ± 0.9 µs

还可以提供控制计时测量方式的参数。有关这些参数的更多信息,请参阅在线文档,但以下代码片段会运行100次迭代,而不是200毫秒内能容纳的任意次数。

>>> for _ in timerit(num=100):
...     sum(range(100000))
Timed for: 100 loops, best of 5
    time per loop: best=616.866 µs, mean=619.120 ± 5.3 µs

自动导入

如果您想使timerit更易于交互式使用,可以将导入移动到PYTHONSTARTUP文件。如果已定义,则该环境变量给出一个Python脚本的路径,该脚本将在每次交互式会话之前执行。例如

$ export PYTHONSTARTUP=~/.pythonrc
$ cat $PYTHONSTARTUP
import timerit
$ python
>>> for _ in timerit:
...     sum(range(100000))
...
Timed for: 59 loops, best of 3
    time per loop: best=2.532 ms, mean=3.309 ± 1.0 ms

程序性使用

timerit库还提供了一个Timerit类,它可以被程序性地使用。

>>> import math, timerit
>>> for timer in timerit:
>>>     setup_vars = 10000
>>>     with timer:
>>>         math.factorial(setup_vars)
>>> print('t1.total_time = %r' % (t1.total_time,))
Timing for 200 loops
Timed for: 200 loops, best of 3
    time per loop: best=2.064 ms, mean=2.115 ± 0.05 ms
t1.total_time = 0.4427177629695507

一个常见的模式是创建一个单一的Timerit实例,然后通过不同的标签重复“重置”它以测试多个不同的算法。这种方式分配的标签将包含在Timerit实例生成的报告字符串中。下面的“基准配方”显示了这种模式的例子。同样,examples/目录中的所有脚本也是如此。

还有一个类似于timeit的IPython魔法的单行代码

比较timeit版本

>>> %timeit math.factorial(100)
564 ns ± 5.46 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

与Timerit版本

>>> Timerit(100000).call(math.factorial, 100).print()
Timed for: 1 loops, best of 1
    time per loop: best=4.828 µs, mean=4.828 ± 0.0 µs

工作原理

timerit模块定义了timerit.Timerit,这是一个可迭代的对象,它产生timerit.Timer上下文管理器。

>>> import math
>>> from timerit import Timerit
>>> for timer in Timerit(num=200, verbose=2):
>>>     with timer:
>>>         math.factorial(10000)

计时上下文管理器通过在__enter__上“tic”和在__exit__上“toc”来测量其主体所需的时间。父Timerit对象可以访问上下文管理器,因此能够读取其测量值。这些测量值被存储,然后我们对它们进行一些统计计算。值得注意的是,分组(批处理)运行时间的最小值、平均值和标准差。

在循环内使用with语句的好处是,您可以在进入上下文管理器之前运行不受计时的设置代码。

在没有设置代码需求的情况下,还有更简洁的语法版本。

>>> import math
>>> from timerit import Timerit
>>> for _ in Timerit(num=200, verbose=2):
>>>     math.factorial(10000)

如果上下文管理器从未被调用,Timerit对象会检测到这一点,并在Timerit对象本身的__iter__方法中进行测量。我相信这种方法比with语句版本的开销略大。(我看到了一些证据表明这实际上可能更准确,但需要进一步测试)。

基准配方

import ubelt as ub
import pandas as pd
import timerit

def method1(x):
    ret = []
    for i in range(x):
        ret.append(i)
    return ret

def method2(x):
    ret = [i for i in range(x)]
    return ret

method_lut = locals()  # can populate this some other way

ti = timerit.Timerit(100, bestof=10, verbose=2)

basis = {
    'method': ['method1', 'method2'],
    'x': list(range(7)),
    # 'param_name': [param values],
}
grid_iter = ub.named_product(basis)

# For each variation of your experiment, create a row.
rows = []
for params in grid_iter:
    key = ub.repr2(params, compact=1, si=1)
    kwargs = params.copy()
    method_key = kwargs.pop('method')
    method = method_lut[method_key]
    # Timerit will run some user-specified number of loops.
    # and compute time stats with similar methodology to timeit
    for timer in ti.reset(key):
        # Put any setup logic you dont want to time here.
        # ...
        with timer:
            # Put the logic you want to time here
            method(**kwargs)
    row = {
        'mean': ti.mean(),
        'min': ti.min(),
        'key': key,
        **params,
    }
    rows.append(row)

# The rows define a long-form pandas data array.
# Data in long-form makes it very easy to use seaborn.
data = pd.DataFrame(rows)
print(data)

plot = True
if plot:
    # import seaborn as sns
    # kwplot autosns works well for IPython and script execution.
    # not sure about notebooks.
    import kwplot
    sns = kwplot.autosns()

    # Your variables may change
    ax = kwplot.figure(fnum=1, doclf=True).gca()
    sns.lineplot(data=data, x='x', y='min', hue='method', marker='o', ax=ax)
    ax.set_title('Benchmark Name')
    ax.set_xlabel('x-variable description')
    ax.set_ylabel('y-variable description')

由支持