While循环比for循环快1000倍?

3

关于for循环和while循环速度的问题已经被问过很多次。通常情况下,for循环应该更快。
然而,在我测试Python 3.5.1时,结果如下:

timeit.timeit('for i in range(10000): True', number=10000)
>>> 12.697646026868842
timeit.timeit('while i<10000: True; i+=1',setup='i=0', number=10000)
>>> 0.0032265179766799434

while循环比for循环快了3000多倍!我还尝试为for循环预生成列表:

timeit.timeit('for i in lis: True',setup='lis = [x for x in range(10000)]', number=10000)
>>> 3.638794646750142
timeit.timeit('while i<10000: True; i+=1',setup='i=0', number=10000)
>>> 0.0032454974941904524

这使得for循环的速度提高了3倍,但差距仍然是3个数量级。

为什么会发生这种情况呢?


我无法完全理解您所得到的时间差的规模;创建一个 range() 对象是有成本的,但成本并不是那么高。即使在 Python 2 中,我也无法让它一直运行到 12.7 秒。 - Martijn Pieters
啊,我的笔记本电脑只是快了6倍而已。 - Martijn Pieters
你正在运行哪个Python版本?这是一个调试版本吗,差异更大吗? - Alexander Oh
@Alex Python 3.5.1。我的笔记本电脑速度很慢,所以我预计会有很长的绝对时间。但是你是否得到了非常不同的相对差异? - ZyTelevan
我有和Martijn几乎一样的笔记本电脑。 - Alexander Oh
2个回答

13
你正在创建10k个range()对象。这需要一些时间来生成。然后,你还需要为这10k个对象创建迭代器对象(用于for循环遍历值)。接下来,for循环通过调用结果迭代器上的__next__方法使用迭代器协议。最后两个步骤也适用于列表上的for循环。

但最重要的是,你在作弊while循环测试。 while 循环只需要运行一次,因为你从不将 i 重置回 0(感谢Jim Fasarakis Hilliard指出这一点)。 实际上,你通过总共19999次比较运行了一个 while 循环; 第一次测试运行了10k次比较,其余9999次测试运行了一次比较。而且那个比较是快速完成的:

>>> import timeit
>>> timeit.timeit('while i<10000: True; i+=1',setup='i=0', number=10000)
0.0008302750065922737
>>> (
...     timeit.timeit('while i<10000: True; i+=1', setup='i=0', number=1) +
...     timeit.timeit('10000 < 10000', number=9999)
... )
0.0008467709994874895

看看这些数字是多么接近?

我的机器稍微快一点,所以让我们创建一个基准来进行比较;这是在运行OS X 10.12.5的Macbook Pro(Retina,15英寸,2015年中期)上使用3.6.1。同时,让我们修复while循环,在测试中将i = 0设置为不是设置(只运行一次):

>>> import timeit
>>> timeit.timeit('for i in range(10000): pass', number=10000)
1.9789885189966299
>>> timeit.timeit('i=0\nwhile i<10000: True; i+=1', number=10000)
5.172155902953818

抱歉,所以一个正确运行的while实际上是更慢的,你的前提(和我的)就这样消失了!我使用pass来避免回答关于引用该对象有多快的问题(它很快,但不是重点)。我的计时将比您的机器快6倍。如果您想探索为什么迭代更快,可以计算Python中for循环的各个组件的时间,从创建range()对象开始:
>>> timeit.timeit('range(10000)', number=10000)
0.0036197409499436617

因此创建10000个range()对象需要比迭代10000次的单个while循环更多的时间。range()对象的创建比整数更昂贵。

这包括全局名称查找,这会更慢,您可以使用setup='_range = range'来使其更快,然后使用_range(1000);这可以缩短约1/3的时间。

接下来,为此创建一个迭代器;在这里,我将使用iter()函数的本地名称,因为for循环不必进行哈希表查找,而是直接访问C函数。当然,对于二进制中内存位置的硬编码引用要快得多:

>>> timeit.timeit('_iter(r)', setup='_iter = iter; r = range(10000)', number=10000)
0.0009729859884828329

相当快,但是它需要与单个 while 循环迭代10k次花费同样的时间。因此创建可迭代对象是廉价的。C实现仍然更快。我们尚未进行迭代。
最后,我们在迭代器对象上调用__next__ 10k次。这再次是在C代码中完成的,具有对内部C实现的缓存引用,但是使用 functools.partial() 对象,我们至少可以尝试得到一个大概的数字:
>>> timeit.timeit('n()', setup='from functools import partial; i = iter(range(10000)); n = partial(i.__next__)', number=10000) * 10000
7.759470026940107

小伙子,对于iter(range(1000)).__next__这样的操作,需要进行10k次调用,而使用for循环则只需要更短的时间;这说明实际的C语言实现非常高效。

然而,这也表明在C代码中循环速度更快,这就是为什么正确执行时,while循环实际上比较慢的原因;在字节码中对整数求和和做布尔比较所需的时间比在C代码中迭代range()所需的时间更长(在C代码中,CPU直接在CPU寄存器中进行递增和比较):

>>> (
...     timeit.timeit('9999 + 1', number=10000 ** 2) +
...     timeit.timeit('9999 < 10000', number=10000 ** 2)
... )    
3.695550534990616

正是这些操作使得while循环变慢了大约3秒钟。


TLDR:您实际上没有正确测试 while 循环。我早些时候应该注意到这一点。

3
您的时间掌握有误,setup仅在第一次执行,之后所有运行的值i都是10000。请参阅timeit文档:

计时主语句的执行次数。此函数只执行一次setup语句,然后返回多次执行主语句所需的时间(以浮点数秒为单位)。

您还可以通过打印每次重复的i来进行验证:

>>> timeit('print(i)\nwhile i<10000: True; i+=1',setup='i=0', number=5)
0
10000
10000
10000
10000

因此,所有随后的运行仅执行比较(即True),并且很快就会结束。

正确计时并查看for循环实际上更快:

>>> timeit('i=0\nwhile i<10000: True; i+=1', number=10000)
8.416439056396484
>>> timeit('for i in range(10000): True', number=10000)
5.589155912399292

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接