为什么第一个 运行 timeit 通常比后面的慢?
Why is the first run of timeit usually slower than subsequent ones?
我一直在测试对一段代码的一些优化(具体来说,elif n in [2,3]
是否比 elif n == 2 or n == 3
快)并注意到一些奇怪的事情。
使用 timeit.default_timer
我对函数的每个版本做了几个 运行,第一个 运行 总是比后续版本慢得多(值从 0.01 左右开始第二个一直下降到 0.003 左右)。
python 是否在幕后做一些事情来优化以后 运行 的代码?这无论如何都不是真正的问题,但我很想知道发生了什么(如果有的话)
是的,python
在第一个 运行 之后缓存 pyc
文件,因此如果代码不更改,它将在下一次迭代中更快 运行 因为它不需要再次将其编译为 byte
代码。请记住 python
是一种解释型语言,它只是跳过 interpretation
步骤。
CPython 上没有这样的通用优化,参考 Python 实现。可能会发生各种更具体的事情,但我们无法确定是什么。
Marcos 的回答表明它是 pyc
文件创建,但这不是 timeit
的工作方式,即使您自己调用 timeit.default_timer
(您不应该这样做 - 您应该使用 timeit.timeit
或 timeit.repeat
或其他此类机制)。
pyc
文件是在导入没有 pyc
文件或其 pyc
文件已过期的模块时创建的。它们不是为 timeit
片段创建的,即使您的计时代码来自导入的模块,典型的 timeit
使用模式也会在计时开始前导入模块。
您正在调用 timeit.default_timer
而不是让 timeit
以其设计的方式处理事情,但即便如此,任何 pyc
文件创建也不太可能在规定时间内发生代码。
PyPy,另一种 Python 实现,使用 JIT 编译,但如果您使用的是 PyPy,您可能会知道。
Numba,一个用于加速数值计算的库,有自己的JIT机制,这也可能导致第一个运行之后的加速。在不注意的情况下依赖 Numba 比在不注意的情况下 运行 在 PyPy 上更容易。
在随后的 运行 中,内存分配可能会更快,具体取决于您使用的类型和它们如何与内存管理系统交互的详细信息,以及您的 malloc
行为方式.例如,在第一个 运行.
之后可能会有更多内存块的空闲列表
还有其他可能性,但最终,我们无法真正判断发生了什么。
一个主要考虑因素是导入的开销可能只会影响 timeit 调用的第一轮。
考虑这个例子:
from timeit import timeit
for _ in range(5):
print(timeit('import requests', number=1))
输出将类似于:
0.1009
1.8307e-05
1.6907e-05
1.6800e-05
1.6817e-05
关于导入的背景知识很少,第一次遇到导入时,需要做一些工作才能将其添加到命名空间,随后导入同一模块几乎是空操作。结果表明 'requests' 是第一次调用时加载的。在 timeit 调用之前和之后打印 globals() 证实了这一点。在模块顶部插入 'import requests' 将导致第一个结果与其他结果相同,再次证实了该理论但并不总是一个实际的解决方案。
增加轮数将减少第一轮的影响,但它可能仍然很重要。在这种情况下,number=100000 为第一次调用提供 0.12 (0.10 + 1.70e-5*100000),为其他调用提供 0.017 (1.70e-5*100000)。
为了它的价值,我有一个一次性的 number=1 调用 timeit 然后继续它。
我一直在测试对一段代码的一些优化(具体来说,elif n in [2,3]
是否比 elif n == 2 or n == 3
快)并注意到一些奇怪的事情。
使用 timeit.default_timer
我对函数的每个版本做了几个 运行,第一个 运行 总是比后续版本慢得多(值从 0.01 左右开始第二个一直下降到 0.003 左右)。
python 是否在幕后做一些事情来优化以后 运行 的代码?这无论如何都不是真正的问题,但我很想知道发生了什么(如果有的话)
是的,python
在第一个 运行 之后缓存 pyc
文件,因此如果代码不更改,它将在下一次迭代中更快 运行 因为它不需要再次将其编译为 byte
代码。请记住 python
是一种解释型语言,它只是跳过 interpretation
步骤。
CPython 上没有这样的通用优化,参考 Python 实现。可能会发生各种更具体的事情,但我们无法确定是什么。
Marcos 的回答表明它是 pyc
文件创建,但这不是 timeit
的工作方式,即使您自己调用 timeit.default_timer
(您不应该这样做 - 您应该使用 timeit.timeit
或 timeit.repeat
或其他此类机制)。
pyc
文件是在导入没有 pyc
文件或其 pyc
文件已过期的模块时创建的。它们不是为 timeit
片段创建的,即使您的计时代码来自导入的模块,典型的 timeit
使用模式也会在计时开始前导入模块。
您正在调用 timeit.default_timer
而不是让 timeit
以其设计的方式处理事情,但即便如此,任何 pyc
文件创建也不太可能在规定时间内发生代码。
PyPy,另一种 Python 实现,使用 JIT 编译,但如果您使用的是 PyPy,您可能会知道。
Numba,一个用于加速数值计算的库,有自己的JIT机制,这也可能导致第一个运行之后的加速。在不注意的情况下依赖 Numba 比在不注意的情况下 运行 在 PyPy 上更容易。
在随后的 运行 中,内存分配可能会更快,具体取决于您使用的类型和它们如何与内存管理系统交互的详细信息,以及您的 malloc
行为方式.例如,在第一个 运行.
还有其他可能性,但最终,我们无法真正判断发生了什么。
一个主要考虑因素是导入的开销可能只会影响 timeit 调用的第一轮。
考虑这个例子:
from timeit import timeit
for _ in range(5):
print(timeit('import requests', number=1))
输出将类似于:
0.1009
1.8307e-05
1.6907e-05
1.6800e-05
1.6817e-05
关于导入的背景知识很少,第一次遇到导入时,需要做一些工作才能将其添加到命名空间,随后导入同一模块几乎是空操作。结果表明 'requests' 是第一次调用时加载的。在 timeit 调用之前和之后打印 globals() 证实了这一点。在模块顶部插入 'import requests' 将导致第一个结果与其他结果相同,再次证实了该理论但并不总是一个实际的解决方案。
增加轮数将减少第一轮的影响,但它可能仍然很重要。在这种情况下,number=100000 为第一次调用提供 0.12 (0.10 + 1.70e-5*100000),为其他调用提供 0.017 (1.70e-5*100000)。
为了它的价值,我有一个一次性的 number=1 调用 timeit 然后继续它。