Python 为什么切片代码比过程代码更快?

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()

我有一个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()
    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