Python 为什么使用中间变量的代码比不使用中间变量的代码要快?

Python 为什么使用中间变量的代码比不使用中间变量的代码要快?,python,python-3.x,cpython,python-internals,Python,Python 3.x,Cpython,Python Internals,我遇到过这种奇怪的行为,但没有解释清楚。这些是基准: py -3 -m timeit "tuple(range(2000)) == tuple(range(2000))" 10000 loops, best of 3: 97.7 usec per loop py -3 -m timeit "a = tuple(range(2000)); b = tuple(range(2000)); a==b" 10000 loops, best of 3: 70.7 usec per loop 为什么使用

我遇到过这种奇怪的行为,但没有解释清楚。这些是基准:

py -3 -m timeit "tuple(range(2000)) == tuple(range(2000))"
10000 loops, best of 3: 97.7 usec per loop
py -3 -m timeit "a = tuple(range(2000));  b = tuple(range(2000)); a==b"
10000 loops, best of 3: 70.7 usec per loop
为什么使用变量赋值进行比较比使用带有临时变量的一行程序快27%以上

根据Python文档,垃圾收集在timeit期间被禁用,所以不能这样。这是某种优化吗

结果也可以在Python2.x中重现,尽管程度较低

运行Windows 7、CPython 3.5.1、Intel i7 3.40 GHz、64位操作系统和Python。我试着在英特尔i7 3.60 GHz和Python 3.5.0上运行的另一台机器似乎没有再现结果



使用相同的Python进程运行
timeit.timeit()
@10000循环,分别产生0.703和0.804。尽管程度较轻,但仍能显示。(~12.5%)

这里的第一个问题是,它是可复制的吗?对我们中的一些人来说,至少这是肯定的,尽管其他人说他们没有看到效果。 在Fedora上,平等性测试改为
,因为实际进行的比较似乎与结果无关,并且范围扩大到200000,因为这似乎使效果最大化:

$ python3 -m timeit "a = tuple(range(200000));  b = tuple(range(200000)); a is b"
100 loops, best of 3: 7.03 msec per loop
$ python3 -m timeit "a = tuple(range(200000)) is tuple(range(200000))"
100 loops, best of 3: 10.2 msec per loop
$ python3 -m timeit "tuple(range(200000)) is tuple(range(200000))"
100 loops, best of 3: 10.2 msec per loop
$ python3 -m timeit "a = b = tuple(range(200000)) is tuple(range(200000))"
100 loops, best of 3: 9.99 msec per loop
$ python3 -m timeit "a = b = tuple(range(200000)) is tuple(range(200000))"
100 loops, best of 3: 10.2 msec per loop
$ python3 -m timeit "tuple(range(200000)) is tuple(range(200000))"
100 loops, best of 3: 10.1 msec per loop
$ python3 -m timeit "a = tuple(range(200000));  b = tuple(range(200000)); a is b"
100 loops, best of 3: 7 msec per loop
$ python3 -m timeit "a = tuple(range(200000));  b = tuple(range(200000)); a is b"
100 loops, best of 3: 7.02 msec per loop
我注意到,运行之间的变化以及表达式运行的顺序对结果的影响很小

将分配给
a
b
的任务添加到慢速版本不会加快速度。事实上,正如我们可能期望的那样,分配给局部变量的效果可以忽略不计。唯一能加速它的是将表达式完全拆分为两部分。这应该造成的唯一区别是,它减少了Python在计算表达式时使用的最大堆栈深度(从4减少到3)

这给了我们一个线索,即效果与堆栈深度有关,可能是额外的级别将堆栈推入另一个内存页。如果是这样的话,我们应该看到,做出影响堆栈的其他更改将改变(很可能会消除这种影响),事实上,这就是我们所看到的:

$ python3 -m timeit -s "def foo():
   tuple(range(200000)) is tuple(range(200000))" "foo()"
100 loops, best of 3: 10 msec per loop
$ python3 -m timeit -s "def foo():
   tuple(range(200000)) is tuple(range(200000))" "foo()"
100 loops, best of 3: 10 msec per loop
$ python3 -m timeit -s "def foo():
   a = tuple(range(200000));  b = tuple(range(200000)); a is b" "foo()"
100 loops, best of 3: 9.97 msec per loop
$ python3 -m timeit -s "def foo():
   a = tuple(range(200000));  b = tuple(range(200000)); a is b" "foo()"
100 loops, best of 3: 10 msec per loop

因此,我认为这完全是由于计时过程中消耗了多少Python堆栈造成的。但这仍然很奇怪。

我的结果与你的结果相似:使用中间变量的代码在Python3.4中始终比你快至少10-20%。但是,当我在同一个Python 3.4解释器上使用IPython时,我得到了以下结果:

In [1]: %timeit -n10000 -r20 tuple(range(2000)) == tuple(range(2000))
10000 loops, best of 20: 74.2 µs per loop

In [2]: %timeit -n10000 -r20 a = tuple(range(2000));  b = tuple(range(2000)); a==b
10000 loops, best of 20: 75.7 µs per loop
值得注意的是,当我在命令行中使用
-mtimeit
时,我从未设法接近前者的74.2µs

所以这只海森堡是很有趣的东西。我决定使用
strace
运行命令,确实有一些可疑之处:

% strace -o withoutvars python3 -m timeit "tuple(range(2000)) == tuple(range(2000))"
10000 loops, best of 3: 134 usec per loop
% strace -o withvars python3 -mtimeit "a = tuple(range(2000));  b = tuple(range(2000)); a==b"
10000 loops, best of 3: 75.8 usec per loop
% grep mmap withvars|wc -l
46
% grep mmap withoutvars|wc -l
41149
这是造成这种差异的一个很好的原因。不使用变量的代码导致调用
mmap
系统调用的次数比使用中间变量的调用次数多近1000倍

对于256k区域,
withoutvars
充满了
mmap
/
munmap
;这些相同的行重复了一遍又一遍:

mmap(NULL, 262144, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f32e56de000
munmap(0x7f32e56de000, 262144)          = 0
mmap(NULL, 262144, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f32e56de000
munmap(0x7f32e56de000, 262144)          = 0
mmap(NULL, 262144, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f32e56de000
munmap(0x7f32e56de000, 262144)          = 0


mmap
调用似乎来自
对象/obmalloc.c
中的函数
\u PyObject\u ArenaMmap
obmalloc.c
还包含宏
ARENA_SIZE
,它是
#定义
d为
(256比较
dis.dis(“tuple(range(2000))==tuple(range(2000))”
dis.dis(“a=tuple(range(2000));b=tuple(range(2000));a==b”)
。在我的配置中,第二个代码段实际上包含了第一个代码段中的所有字节码,以及一些附加指令。很难相信更多字节码指令会导致更快的执行。可能是特定Python版本中的某些错误?如果您试图重现此错误,请在不同的执行中多次运行测试orders.-不管结果如何,也不管这个问题有多奇怪,我认为这个问题对SO来说并不是特别有价值。我认为这很有趣。@您需要记住,对于类似现象的答案现在是stackoverflow中投票最多的答案。另外,请尝试使用
tim在单个Python进程中运行测试eit
模块。两个单独的Python进程之间的比较可能会受到操作系统的任务调度程序或其他效果的影响。@aluriak“三选一”表示三个平均值中的最佳值。之所以这样做,是因为某些平均值可能包括(例如)意外的进程暂停。采用最佳平均值可以避免这种情况。但是,两台具有相同内存条和相同操作系统的计算机会导致不同的结果。堆栈深度听起来是一个不错的理论,但它无法解释计算机之间的差异。哇,这是相互关联的测试。垃圾收集器(在timeit上被禁用)不应该负责释放还是至少应该负责释放?它提出了另一个问题:这些重复的调用不是一个bug吗?@Bharel更像是“按设计破坏”@Bharel这取决于是否分配了新的内存竞技场;很可能其他系统的部分可用竞技场在池中有足够的可用内存,而不需要更多。即使是表面上相似的系统上的相同Python版本也可能有不同的行为,比如Python安装路径、pac数量
站点包中的kages
、环境变量、当前工作目录-它们都会影响进程的内存布局。@Bharel:CPython中的垃圾收集器更恰当地称为“循环垃圾收集器”;它只关心释放孤立的引用周期,而不是一般的垃圾收集。所有其他清理都是同步的,并且有序;如果释放对竞技场中最后一个对象的最后一个引用,则该对象将立即被删除,竞技场将立即被释放,不需要循环垃圾收集器参与。这就是为什么它是合法的禁用
gc
;如果禁用了常规清理,则
for n in range(10000)
    a = tuple(range(2000))
    b = tuple(range(2000))
    a == b