Python 为什么切片代码比过程代码更快?
我有一个Python函数,它获取一个列表并返回一个生成器,生成每个相邻对的2个元组,例如Python 为什么切片代码比过程代码更快?,python,performance,list,slice,Python,Performance,List,Slice,我有一个Python函数,它获取一个列表并返回一个生成器,生成每个相邻对的2个元组,例如 >>> list(pairs([1, 2, 3, 4])) [(1, 2), (2, 3), (3, 4)] 我考虑了一个使用2个切片的实现: def pairs(xs): for p in zip(xs[:-1], xs[1:]): yield p 还有一个是以更程序化的方式编写的: def pairs(xs): last = object()
>>> list(pairs([1, 2, 3, 4]))
[(1, 2), (2, 3), (3, 4)]
我考虑了一个使用2个切片的实现:
def pairs(xs):
for p in zip(xs[:-1], xs[1:]):
yield p
还有一个是以更程序化的方式编写的:
def pairs(xs):
last = object()
dummy = last
for x in xs:
if last is not dummy:
yield last,x
last = x
使用范围(2**15)
作为输入进行测试会产生以下时间(您可以找到我的测试代码和输出):
无切片实现的部分性能影响是循环中的比较(如果last不是dummy
)。删除该选项(使输出不正确)可以提高其性能,但仍比zip-a-pair-of-Slice实现慢:
2 slices: 100 loops, best of 3: 4.48 msec per loop
0 slices: 100 loops, best of 3: 5.2 msec per loop
def pairs(xs):
for ii in range(1,len(xs)):
yield xs[ii-1], xs[ii]
所以,我被难住了。为什么将两个片段压缩在一起,有效地并行迭代列表两次,比迭代一次更快,同时更新last
和x
编辑
第三个实施:
2 slices: 100 loops, best of 3: 4.48 msec per loop
0 slices: 100 loops, best of 3: 5.2 msec per loop
def pairs(xs):
for ii in range(1,len(xs)):
yield xs[ii-1], xs[ii]
下面是它与其他实现的比较:
2 slices: 100 loops, best of 3: 4.37 msec per loop
0 slices: 100 loops, best of 3: 5.61 msec per loop
Lenski's: 100 loops, best of 3: 6.43 msec per loop
更慢!这让我很困惑
编辑2:
使用itertools.izip
而不是zip
,它甚至比zip
更快:
2 slices, izip: 100 loops, best of 3: 3.68 msec per loop
因此,
izip
是目前为止的赢家!但仍有难以检查的原因。我怀疑第二个版本速度较慢的主要原因是,它对每一对产生的s进行比较操作:
# pair-generating loop
for x in xs:
if last is not dummy:
yield last,x
last = x
第一个版本除了输出值之外什么都不做。重命名循环变量后,它等效于:
# pair-generating loop
for last,x in zip(xs[:-1], xs[1:]):
yield last,x
它不是特别漂亮或Pythonic,但是您可以编写一个过程版本,而不需要在内部循环中进行比较。这个跑多快
def pairs(xs):
for ii in range(1,len(xs)):
yield xs[ii-1], xs[ii]
这是iZip
的结果,它实际上更接近您的实现。看起来就像你期望的那样。zip
版本正在函数中的内存中创建整个列表,因此它是最快的。循环版本只是通过列表,所以它稍微慢一点。izip
与代码最为相似,但我猜有一些内存管理后端进程会增加执行时间
In [11]: %timeit pairsLoop([1,2,3,4,5])
1000000 loops, best of 3: 651 ns per loop
In [12]: %timeit pairsZip([1,2,3,4,5])
1000000 loops, best of 3: 637 ns per loop
In [13]: %timeit pairsIzip([1,2,3,4,5])
1000000 loops, best of 3: 655 ns per loop
根据要求,代码版本如下所示:
from itertools import izip
def pairsIzip(xs):
for p in izip(xs[:-1], xs[1:]):
yield p
def pairsZip(xs):
for p in zip(xs[:-1], xs[1:]):
yield p
def pairsLoop(xs):
last = object()
dummy = last
for x in xs:
if last is not dummy:
yield last,x
last = x
在这篇文章的其他地方有很多有趣的讨论。基本上,我们从比较这个函数的两个版本开始,我将用以下愚蠢的名称来描述:
“zip
-py”版本:
“循环”版本:
那么,为什么循环版本会变慢呢?基本上,我认为可以归结为两件事:
loopy版本显式地做额外的工作:它在生成内部循环迭代的每一对上比较两个对象的标识(如果last不是dummy:…
)
- @mambocab的编辑显示,不进行这种比较确实会导致循环版本
稍微快一点,但不能完全缩小差距。
zippy版本在编译的C代码中所做的工作比loopy版本在Python代码中所做的要多:
- 将两个对象组合成一个
元组
。循环版本确实产生最后一个x
,而在压缩版本中,元组p
直接来自zip
,因此它只产生p
将变量名绑定到对象:循环版本在每个循环中执行两次,在for
循环中分配x
,并last=x
。zippy版本只在for
循环中执行一次
有趣的是,zippy版本实际上有一种方式在做更多的工作:它使用两个listiterator
s、iter(xs[:-1])
和iter(xs[1:])
,它们被传递到zip
。循环版本仅使用一个列表迭代器
(用于xs中的x
)
- 创建
listiterator
对象(iter([])
的输出)可能是一个高度优化的操作,因为Python程序员经常使用它
xs[:-1]
和xs[1://code>,是一个非常轻量级的操作,与在整个列表上迭代相比,几乎不增加任何开销。本质上,它只意味着移动迭代器的起点或终点,而不是改变每次迭代中发生的事情
如果您
导入dis
并使用dis.dis(pairs)
作为函数和组件,这可能会有所帮助。我认为zip
实际上会在开始生成之前在内存中创建整个列表。itertools.izip
的性能是什么样子的?这与您的过程代码非常接近xs
列表的Python级索引可能会减慢速度;有一些涉及范围检查。除此之外,我不知道为什么我的版本会更慢。。。该死!您的pairsLoop
版本是什么?我不认为zip
和izip
版本之间应该有很大的区别,只要整个列表可以很容易地放入内存。可能的罪魁祸首是OP的循环版本在每个循环中都做了额外的工作,正如。我已经更新了帖子以包含所有内容。我没有更改OP中的任何代码。您运行的测试与OP有些不同:您使用短列表进行了许多(1000000)次迭代(range(1,6)
),而OP使用长列表进行了几(100)次迭代(range(2**15)
)。这很可能会有所不同……我不认为我传达得很清楚,但我在问题中提到,我在没有比较的情况下尝试了它(使输出不正确)。时间安排是我问题的最底层——比较似乎是运行时的一个很好的部分,但仍然比
def pairs(xs):
last = object()
dummy = last
for x in xs:
if last is not dummy:
yield last,x
last = x