虽然循环比循环快1000倍?

时间:2017-06-26 12:34:59

标签: python performance python-3.x loops

因此,之前已多次询问有关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个数量级。

为什么会这样?

2 个答案:

答案 0 :(得分:11)

您正在创建10k range()个对象。这需要一些时间来实现。然后,您必须为这些10k对象创建iterator objects(对于for循环来迭代值)。接下来,for循环通过调用生成的迭代器上的__next__ method来使用迭代器协议。后两个步骤也适用于列表上的for循环。

但最重要的是,你在while循环测试中作弊while循环只需运行一次,因为您永远不会将i重置回0(感谢Jim Fasarakis Hilliard pointing that out)。您实际上在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()个对象比运行迭代10k次的单个while循环花费更多时间。 range()对象的创建成本比整数更高。

这确实涉及全局名称查找,速度较慢,您可以使用setup='_range = range'然后使用_range(1000)来加快速度。这大约是时间的1/3。

接下来,为此创建一个迭代器;在这里,我将使用iter() function的本地名称,因为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() object我们至少可以尝试来获得球场图:

>>> timeit.timeit('n()', setup='from functools import partial; i = iter(range(10000)); n = partial(i.__next__)', number=10000) * 10000
7.759470026940107

男孩,10k次拨打iter(range(1000)).__next__的10k次呼叫的时间比管理的for循环多出近4倍;这表明实际C实现的效率如何。

但是,它确实说明了C代码中的循环要快得多,这就是为什么while循环在正确执行时实际上更慢的原因;求和整数并在字节码中进行布尔比较比在C代码中迭代range()需要更多的时间(其中CPU直接在CPU寄存器中进行递增和比较):

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

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

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

答案 1 :(得分:2)

您错误地计时,setup只执行一次,然后{em}的值为i 所有后续运行。请参阅timeit上的文档:

  

主要陈述的时间编号执行。这将执行一次10000语句,然后返回执行主语句多次所需的时间,以秒为单位测量为浮点数。

另外,每次重复打印setup进行验证:

i

因此,所有后续运行仅执行比较(>>> timeit('print(i)\nwhile i<10000: True; i+=1',setup='i=0', number=5) 0 10000 10000 10000 10000 )并提前完成。

正确的时间,看看True循环实际上是如何更快的:

for