36

我正在优化一些 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):

即使循环内容相同。

什么给出了,这里有更一般的编程课程吗?

4

10 回答 10

84

我可以在我的 Q6600 (Python 2.6.2) 上重现这个;将范围增加到 100000000:

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

首先,一些观察:

  • 对于一个简单的操作,这是 5%。这很重要。
  • 本机加法和减法操作码的速度无关紧要。它在本底噪声中,与字节码评估相比完全相形见绌。那是在谈论成千上万的一两个本机指令。
  • 字节码生成完全相同数量的指令;唯一的区别是INPLACE_ADDvs.INPLACE_SUBTRACT和 +1 vs -1。

查看 Python 源代码,我可以做出猜测。这在 ceval.c 中处理,在PyEval_EvalFrameEx. INPLACE_ADD有一个重要的额外代码块来处理字符串连接。该块在 中不存在INPLACE_SUBTRACT,因为您不能减去字符串。这意味着INPLACE_ADD包含更多本机代码。取决于(严重!)编译器生成代码的方式,这些额外的代码可能与 INPLACE_ADD 代码的其余部分内联,这意味着加法比减法更难命中指令缓存。这可能会导致额外的二级缓存命中,从而导致显着的性能差异。

这在很大程度上取决于您所在的系统(不同的处理器具有不同数量的缓存和缓存架构),使用的编译器,包括特定的版本和编译选项(不同的编译器将不同地决定哪些代码位是关键路径,它决定了汇编代码如何集中在一起),等等。

此外,Python 3.0.1 中的差异是相反的(+:15.66,-:16.71);毫无疑问,这个关键功能已经发生了很大变化。

于 2009-09-08T22:47:51.943 回答
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

看起来你有一些测量偏差

于 2009-09-08T22:09:23.127 回答
7

我认为“一般编程课”是很难仅通过查看源代码来预测哪个语句序列将是最快的。各级程序员经常被这种“直观”的优化所吸引。你认为你知道的不一定是真的。

实际测量您的程序性能是无可替代的。这样做的荣誉;在这种情况下,回答为什么无疑需要深入研究 Python 的实现。

对于 Java、Python 和 .NET 等字节编译语言,仅在一台机器上测量性能甚至是不够的。VM 版本、本机代码翻译实现、特定于 CPU 的优化等之间的差异将使这类问题变得更加难以回答。

于 2009-09-08T22:27:52.203 回答
5

“第二个循环确实更快......”

这就是你的解释。重新排序您的脚本,使减法测试首先计时,然后是加法,然后突然加法再次成为更快的操作:

-= 3.05
+= 2.84

显然,脚本的后半部分发生了一些事情,使它更快。我的猜测是,第一次调用range()比较慢,因为 python 需要为这么长的列表分配足够的内存,但它能够重新使用该内存进行第二次调用range()

import time
start = time.clock()
x = range(10000000)
end = time.clock()
del x
print 'first range()',end-start
start = time.clock()
x = range(10000000)
end = time.clock()
print 'second range()',end-start

该脚本的几次运行表明,第一次所需的额外时间range()几乎占了上面看到的 '+=' 和 '-=' 之间的所有时间差:

first range() 0.4
second range() 0.23
于 2009-09-09T00:55:59.800 回答
4

当问一个问题时,说出你正在使用什么平台和什么版本的 Python 总是一个好主意。有时没关系。这不是那些时代之一:

  1. time.clock()仅适用于 Windows。扔掉您自己的测量代码并-m timeit按照 pixelbeat 的答案使用。

  2. Python 2.Xrange()构建了一个列表。如果您使用的是 Python 2.x,请替换rangexrange并查看会发生什么。

  3. Python 3.Xint是 Python2.X 的long.

于 2009-09-08T22:46:51.073 回答
2
这里有更一般的编程课程吗?

这里更一般的编程课程是,在预测计算机代码的运行时性能时,直觉是一个糟糕的指导。

可以推理算法复杂性、关于编译器优化的假设、估计缓存性能等等。但是,由于这些东西可以以非平凡的方式进行交互,因此确定一段特定代码的速度的唯一方法是在目标环境中对其进行基准测试(正如您所做的那样)。

于 2010-11-02T13:46:31.030 回答
0

那将是了不起的,所以我已经彻底评估了您的代码并设置了 expiriment,因为我发现它更正确(循环外的所有声明和函数调用)。这两个版本我都运行了五次。

  • 运行您的代码验证了您的声明:-= 花费的时间不断减少;平均 3.6%
  • 但是,运行我的代码与您的实验结果相矛盾:+= 平均(并非总是)减少 0.5% 的时间。

为了显示所有结果,我将图放到了网上:

所以,我的结论是你的实验有偏差,而且很重要。

最后是我的代码:

import time

addtimes = [0.] * 100
subtracttimes = [0.] * 100

range100 = range(100)
range10000000 = range(10000000)

j = 0
i = 0
x = 0
start = 0.


for j in range100:
 start = time.clock()
 x = 0
 for i in range10000000:
  x += 1
 addtimes[j] = time.clock() - start

for j in range100:
 start = time.clock()
 x = 0
 for i in range10000000:
  x -= -1
 subtracttimes[j] = time.clock() - start

print '+=', sum(addtimes)
print '-=', sum(subtracttimes)
于 2009-09-09T13:26:04.900 回答
0

对于 Python 2.5,这里最大的问题是使用范围,它将分配一个很大的列表来迭代它。使用 xrange 时,无论第二个完成,对我来说都快一点。(不确定 range 是否已成为 Python 3 中的生成器。)

于 2009-09-08T22:39:06.813 回答
0

你的实验有问题。这个实验的设计方式是编写 2 个不同的程序 - 1 个用于加法,1 个用于减法。它们应该完全相同,并在与要归档的数据相同的条件下运行。然后你需要平均运行次数(至少几千次),但你需要一个统计学家告诉你一个合适的数字。

如果您想分析不同的加法、减法和循环方法,那么每种方法都应该是一个单独的程序。

实验错误可能来自处理器的热量和 cpu 上的其他活动,所以我会以各种模式执行运行......

于 2009-09-09T00:37:51.963 回答
-1

向后运行循环更快,因为计算机比较一个数字是否等于 0 时更容易。

于 2009-09-08T22:12:52.593 回答