number 和 repeat 是单独的参数是因为它们用于不同的目的。 number 控制每个时间执行多少次,它用于获取有代表性的时间。 repeat 参数控制完成了多少次计时,它的用途是获得准确的统计信息。 IPython 使用 mean 或 average 来计算所有重复语句的运行时间,然后将该数字除以 number。所以它测量平均值的平均值。在早期版本中,它使用所有 repeats 的最短时间 (min()) 并将其除以 number 并报告为“最佳”。
要了解为什么有两个参数来控制 number 和 repeats,您必须了解您正在计时以及如何测量时间。 p>
时钟的粒度和执行次数
一台计算机有不同的“时钟”来测量时间。这些时钟有不同的“滴答声”(取决于操作系统)。例如,它可以测量秒、毫秒或纳秒 - 这些刻度称为时钟的粒度。
如果执行的持续时间小于或大致等于时钟的粒度,则无法获得具有代表性的时序。假设您的操作需要 100ns(=0.0000001 秒),但时钟仅测量毫秒(=0.001 秒),那么大多数测量将测量 0 毫秒,少数测量将测量 1 毫秒 - 这取决于时钟周期中执行开始的位置和完成的。这并不能真正代表您想要计时的持续时间。
这是在 time.time 的粒度为 1 毫秒的 Windows 上:
import time
def fast_function():
return None
r = []
for _ in range(10000):
start = time.time()
fast_function()
r.append(time.time() - start)
import matplotlib.pyplot as plt
plt.title('measuring time of no-op-function with time.time')
plt.ylabel('number of measurements')
plt.xlabel('measured time [s]')
plt.yscale('log')
plt.hist(r, bins='auto')
plt.tight_layout()
这显示了此示例中测量时间的直方图。几乎所有测量都是 0 毫秒,三个测量是 1 毫秒:
Windows 上有粒度低得多的时钟,这只是为了说明粒度的影响,每个时钟都有一定的粒度,即使它低于一毫秒。
为了克服粒度的限制,可以增加执行次数,因此预期的持续时间明显高于时钟的粒度。因此,不要在运行 number 次后运行执行。从上面获取数字并使用 100 000 的 数字,预期的运行时间将为 = 0.01 秒。因此,忽略其他所有因素,时钟现在几乎在所有情况下都会测量 10 毫秒,这与预期的执行时间非常相似。
简而言之,指定 number 可衡量 number 次执行的 sum。您需要再次将这种方式的时间测量除以 number 以获得“每次执行时间”。
其他进程和执行的重复次
您的操作系统通常有很多活动进程,其中一些可以并行运行(不同的处理器或使用超线程),但大多数都按操作系统调度时间顺序运行,每个进程在 CPU 上运行。大多数时钟不关心当前运行的进程,因此测量的时间将根据调度计划而有所不同。还有一些时钟不是测量系统时间而是测量过程时间。然而,它们测量 Python 进程的完整时间,其中有时会包括垃圾收集或其他 Python 线程——除此之外,Python 进程不是无状态的,并非每个操作总是完全相同,而且还有内存分配/发生重新分配/清除(有时在幕后),这些内存操作时间可能因多种原因而异。
我再次使用直方图测量在我的计算机上求和一万个所需的时间(仅使用 repeat 并将 number 设置为 1):
import timeit
r = timeit.repeat('sum(1 for _ in range(10000))', number=1, repeat=1_000)
import matplotlib.pyplot as plt
plt.title('measuring summation of 10_000 1s')
plt.ylabel('number of measurements')
plt.xlabel('measured time [s]')
plt.yscale('log')
plt.hist(r, bins='auto')
plt.tight_layout()
此直方图在略低于约 5 毫秒处显示了一个急剧的截止时间,这表明这是可以执行操作的“最佳”时间。较高的时间是测量条件不是最佳的或其他进程/线程花费了一些时间:
避免这些波动的典型方法是经常重复计时次数,然后使用统计数据来获得最准确的数字。哪个统计数据取决于您要测量的内容。我将在下面更详细地介绍这一点。
同时使用 number 和 repeat
基本上%timeit 是timeit.repeat 的包装器,大致相当于:
import timeit
timer = timeit.default_timer()
results = []
for _ in range(repeat):
start = timer()
for _ in range(number):
function_or_statement_to_time
results.append(timer() - start)
但%timeit 与timeit.repeat 相比具有一些便利功能。例如,它根据 repeat 和 number 获得的时间计算 one 执行的最佳和平均时间。
这些大致是这样计算的:
import statistics
best = min(results) / number
average = statistics.mean(results) / number
您还可以使用TimeitResult(如果您使用-o 选项则返回)检查所有结果:
>>> r = %timeit -o ...
7.46 ns ± 0.0788 ns per loop (mean ± std. dev. of 7 runs, 100000000 loops each)
>>> r.loops # the "number" is called "loops" on the result
100000000
>>> r.repeat
7
>>> r.all_runs
[0.7445439999999905,
0.7611092000000212,
0.7249667000000102,
0.7238135999999997,
0.7385598000000186,
0.7338551999999936,
0.7277425999999991]
>>> r.best
7.238135999999997e-09
>>> r.average
7.363701571428618e-09
>>> min(r.all_runs) / r.loops # calculated best by hand
7.238135999999997e-09
>>> from statistics import mean
>>> mean(r.all_runs) / r.loops # calculated average by hand
7.363701571428619e-09
关于 number 和 repeat 值的一般建议
如果您想修改 number 或 repeat,那么您应该将 number 设置为可能的最小值,而不会遇到计时器。根据我的经验,number 应该设置为使函数的 number 执行至少需要 10 微秒(0.00001 秒),否则您可能只会“计时”“计时器”。
repeat 应该设置得尽可能高。重复次数越多,您就越有可能真正找到真正的最佳或平均水平。然而,更多的重复需要更长的时间,因此也需要权衡取舍。
IPython 调整 number 但保持 repeat 不变。我经常做相反的事情:我调整 number 以便语句的 number 执行需要大约 10us,然后我调整我得到的 repeat统计数据的良好表示(通常在 100-10000 范围内)。但您的里程可能会有所不同。
哪个统计数据最好?
timeit.repeat 的文档中提到了这一点:
注意
从结果向量计算平均值和标准差并报告这些是很诱人的。但是,这不是很有用。在典型情况下,最小值给出了机器运行给定代码 sn-p 的速度的下限;结果向量中的较高值通常不是由 Python 速度的可变性引起的,而是由其他进程干扰您的计时精度引起的。所以结果的 min() 可能是您应该感兴趣的唯一数字。之后,您应该查看整个向量并应用常识而不是统计数据。
例如,人们通常想知道算法的速度有多快,然后可以使用这些重复中的最小值。如果人们对时间的平均值或中位数更感兴趣,则可以使用这些测量值。在大多数情况下,第一个最感兴趣的是最小值,因为最小值类似于执行的速度 - 最小值可能是进程中断最少的一次执行(被其他进程、GC 或中断次数最多)最佳内存操作)。
为了说明差异,我再次重复了上述时间,但这次我包括了最小值、平均值和中值:
import timeit
r = timeit.repeat('sum(1 for _ in range(10000))', number=1, repeat=1_000)
import numpy as np
import matplotlib.pyplot as plt
plt.title('measuring summation of 10_000 1s')
plt.ylabel('number of measurements')
plt.xlabel('measured time [s]')
plt.yscale('log')
plt.hist(r, bins='auto', color='black', label='measurements')
plt.tight_layout()
plt.axvline(np.min(r), c='lime', label='min')
plt.axvline(np.mean(r), c='red', label='mean')
plt.axvline(np.median(r), c='blue', label='median')
plt.legend()
与此“建议”相反(请参阅上面引用的文档)IPythons %timeit 报告平均值而不是 min()。然而,他们也只默认使用 7 的 repeat - 我认为这太少了,无法准确确定 minimum - 所以在这种情况下使用平均值实际上是明智的。一个很好的工具来做一个“快速和肮脏”的时间。
如果您需要一些允许根据您的需要对其进行自定义的东西,可以直接使用timeit.repeat,甚至可以使用第 3 方模块。例如: