为什么这个切片代码比更多的过程代码更快?

时间:2014-09-17 00:12:17

标签: 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()
    dummy = last
    for x in xs:
        if last is not dummy:
            yield last,x
        last = x

使用range(2 ** 15)作为输入进行测试会产生以下时间(您可以找到我的测试代码并输出here):

2 slices: 100 loops, best of 3: 4.23 msec per loop
0 slices: 100 loops, best of 3: 5.68 msec per loop

无切片实现的部分性能影响是循环中的比较(if last is not 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

所以,我很难过。为什么要将2个切片压缩在一起,有效地在列表上并行迭代两次,比迭代一次更快,更新lastx

修改

Dan Lenski proposed第三个实施:

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:

ssm suggested使用itertools.izip代替zip,它甚至比zip更快:

2 slices, izip: 100 loops, best of 3: 3.68 msec per loop

所以,到目前为止,izip是赢家!但仍然是出于难以检查的原因。

3 个答案:

答案 0 :(得分:2)

这是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

答案 1 :(得分:2)

这个帖子中的其他地方有很多有趣的讨论。基本上,我们开始比较这个函数的两个版本,我将用以下哑名来描述:

  1. " zip - py"版本:

    def pairs(xs):
        for p in zip(xs[:-1], xs[1:]): 
            yield p
    
  2. " loopy"版本:

    def pairs(xs):
        last = object()
        dummy = last
        for x in xs:
            if last is not dummy:
                yield last,x
            last = x
    
  3. 那么为什么loopy版本变慢?基本上,我认为这取决于几件事:

    1. loopy版本明确地做了额外的工作:它比较了两个对象'内圈的每一对生成迭代的身份(if last is not dummy: ...)。

      • @ mambocab的编辑显示,没有进行此比较确实使循环版本成为了 稍微快一些,但并没有完全缩小差距。

    2. zippy版本在循环版本在Python代码中执行的编译C代码中做了更多的事情:

      • 将两个对象合并为tuple。 loopy版本为yield last,x,而在zippy版本中,元组p直接来自zip,因此只有yield p

      • 将变量名称绑定到object:loopy版本在每个循环中执行两次,在x循环和for中分配last=x。 zippy版本只在for循环中执行此操作一次。

    3. 有趣的是,zippy版本实际上有一种方式更多工作:它使用两个 listiterator s,{{1 }}和iter(xs[:-1]),传递给iter(xs[1:])。 loopy版本仅使用一个 ziplistiterator)。

      • 创建for x in xs对象(listiterator的输出)可能是一个非常高度优化的操作,因为Python程序员经常使用它。
      • 迭代列表切片iter([])xs[:-1]是一个非常轻量级的操作,与迭代整个列表相比,几乎不增加任何开销。本质上,它只是意味着移动迭代器的起点或终点,但不会改变每次迭代时发生的事情。

答案 2 :(得分:1)

我怀疑第二个版本较慢的主要原因是因为它对yield 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]