为什么在Python中减法比加法快?

38 投票
10 回答
6675 浏览
提问于 2025-04-15 14:10

我在优化一些Python代码时,做了一个实验:

import time

start = time.clock()
x = 0
for i in range(10000000):
    x += 1
end = time.clock()

print '+=',end-start

start = time.clock()
x = 0
for i in range(10000000):
    x -= -1
end = time.clock()

print '-=',end-start

第二个循环的速度总是更快,快的幅度从一点点到10%不等,这取决于我运行的系统。我尝试过改变循环的顺序、执行的次数等等,结果还是一样。

更奇怪的是,

for i in range(10000000, 0, -1):

(也就是反向运行循环)比

for i in range(10000000):

还要快,即使循环的内容是完全一样的。

这是怎么回事?这里面有没有更普遍的编程教训呢?

10 个回答

8

我觉得“编程的一般教训”就是,光看源代码真的很难预测出哪一段代码执行得最快。无论是新手还是老手,程序员们常常会被这种“直觉式”的优化搞得晕头转向。你以为你知道的事情,可能并不一定正确。

实际上,唯一能让你了解程序性能的方法就是去测量它的表现。为此而努力是值得赞扬的;想要回答为什么,肯定需要深入研究Python的实现细节。

对于像Java、Python和.NET这样的字节编译语言,仅仅在一台机器上测量性能是不够的。不同的虚拟机版本、原生代码转换实现、针对特定CPU的优化等等,都会让这个问题变得更加复杂。

13
$ python -m timeit -s "x=0" "x+=1"
10000000 loops, best of 3: 0.151 usec per loop
$ python -m timeit -s "x=0" "x-=-1"
10000000 loops, best of 3: 0.154 usec per loop

看起来你遇到了一些测量偏差的问题。

89

我在我的Q6600上可以复现这个问题(Python 2.6.2);把范围增加到100000000:

('+=', 11.370000000000001)
('-=', 10.769999999999998)

首先,有几点观察:

  • 对于一个简单的操作来说,这个差异是5%。这可是个不小的数字。
  • 原生的加法和减法指令速度并不重要。它们的影响微乎其微,完全被字节码的执行所掩盖。这里说的是一两条原生指令和成千上万条字节码指令的对比。
  • 字节码生成的指令数量是完全一样的;唯一的区别在于 INPLACE_ADDINPLACE_SUBTRACT 以及 +1 和 -1。

看一下Python的源代码,我可以做个猜测。这部分是在ceval.c文件中的 PyEval_EvalFrameEx 处理的。INPLACE_ADD 有一段额外的代码块,用来处理字符串的连接。而 INPLACE_SUBTRACT 没有这个代码块,因为你不能对字符串进行减法。这意味着 INPLACE_ADD 包含了更多的原生代码。根据编译器生成代码的方式,这段额外的代码可能和其他 INPLACE_ADD 的代码在一起,这样加法操作可能会比减法操作更容易命中指令缓存。这 可能 导致额外的L2缓存命中,从而造成明显的性能差异。

这很大程度上取决于你使用的系统(不同的处理器有不同的缓存和缓存架构)、所用的编译器,包括特定版本和编译选项(不同的编译器会有不同的决定,哪些代码是关键路径,这决定了汇编代码是如何组合在一起的),等等。

另外,在Python 3.0.1中,这个差异是反过来的(+: 15.66, -: 16.71);毫无疑问,这个关键函数发生了很大的变化。

撰写回答