【问题标题】:-n and -r arguments to IPython's %timeit magicIPython 的 %timeit 魔术的 -n 和 -r 参数
【发布时间】:2018-01-15 06:35:55
【问题描述】:

我想在 Jupyter 笔记本中使用 timeit 魔术命令对代码块进行计时。根据文档,timeit 有几个参数。两个特别控制循环次数和重复次数。我不清楚的是这两个论点之间的区别。例如

import numpy
N = 1000000
v = numpy.arange(N)

%timeit -n 10 -r 500 pass; w = v + v

将运行 10 次循环和 500 次重复。我的问题是,

这可以解释为 下列的? (与实际计时结果有明显差异)

import time
n = 10
r = 500
T = numpy.empty(r)
for j in range(r):
    t0 = time.time()
    for i in range(n):
        w = v + v
    T[j] = (time.time() - t0)/n

print('Best time is {:.4f} ms'.format(max(T)*1000))

我正在做的一个假设(而且很可能是不正确的)是内部循环的时间是通过此循环的n 迭代的平均值。然后取这个循环的 500 次重复中最好的一次。

我已经搜索了文档,但没有找到任何具体说明它在做什么的东西。比如文档here

选项: -n:在循环中执行给定的语句次数。如果没有给出这个值,则选择一个拟合值。

-r:重复循环迭代次数,取最好的结果。默认值:3

没有真正说明内部循环的计时方式。最后的结果是什么中的“最好”?

我要计时的代码不涉及任何随机性,所以我想知道是否应该将这个内部循环设置为n=1。然后,r 重复将处理任何系统可变性。

【问题讨论】:

    标签: python ipython jupyter


    【解决方案1】:

    numberrepeat 是单独的参数是因为它们用于不同的目的。 number 控制每个时间执行多少次,它用于获取有代表性的时间。 repeat 参数控制完成了多少次计时,它的用途是获得准确的统计信息。 IPython 使用 meanaverage 来计算所有重复语句的运行时间,然后将该数字除以 number。所以它测量平均值的平均值。在早期版本中,它使用所有 repeats 的最短时间 (min()) 并将其除以 number 并报告为“最佳”。

    要了解为什么有两个参数来控制 numberrepeats,您必须了解您正在计时以及如何测量时间。 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 毫秒处显示了一个急剧的截止时间,这表明这是可以执行操作的“最佳”时间。较高的时间是测量条件不是最佳的或其他进程/线程花费了一些时间:

    避免这些波动的典型方法是经常重复计时次数,然后使用统计数据来获得最准确的数字。哪个统计数据取决于您要测量的内容。我将在下面更详细地介绍这一点。

    同时使用 numberrepeat

    基本上%timeittimeit.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)
    

    %timeittimeit.repeat 相比具有一些便利功能。例如,它根据 repeatnumber 获得的时间计算 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
    

    关于 numberrepeat 值的一般建议

    如果您想修改 numberrepeat,那么您应该将 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 方模块。例如:

    【讨论】:

      【解决方案2】:

      看起来最新版本的%timeit 取的是r n-loop 平均值的平均值,而不是最好的平均值。

      显然,这与早期版本的 Python 不同。 r 平均值的最佳时间仍然可以通过TimeResults 返回参数获得,但不再是显示的值。

      评论:我最近从上面运行了这段代码,发现以下语法不再有效:

      n = 1
      r = 50
      tr = %timeit -n $n -r $r -q -o pass; compute_mean(x,np)
      

      也就是说,不再可能(似乎)使用$var 将变量传递给timeit 魔术命令。这是否意味着这个神奇的命令应该被淘汰并替换为timeit 模块?

      我正在使用 Python 3.7.4。

      【讨论】:

      • 是的,这似乎是一个实现细节!至少在较旧的 IPython 版本上,它是最好的,而不是平均水平。
      • 你这里没有回答主要问题,-r 和 -n 在这里到底是做什么的?
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2018-07-21
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-04-23
      • 1970-01-01
      相关资源
      最近更新 更多