【问题标题】:Why is this slicing code faster than more procedural code?为什么这个切片代码比更多程序代码更快?
【发布时间】:2014-11-10 21:14:46
【问题描述】:

我有一个 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-slices 实现慢:

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 个切片压缩在一起,有效地并行迭代列表两次,比迭代一次更快,随时更新 last 和 x?

编辑

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 是迄今为止的赢家!但仍然出于难以检查的原因。

【问题讨论】:

  • 如果您 import dis 并使用 dis.dis(pairs) 进行功能和比较,它可能会有所帮助。
  • 我认为zip 实际上在开始执行产量之前在内存中创建了整个列表。 itertools.izip 的性能怎么样?那更接近你的一段程序代码
  • xs 列表的 Python 级索引可能会减慢速度;涉及到一些范围检查。除此之外,我不确定为什么我的版本会更慢...... drat!

标签: python performance list slice


【解决方案1】:

在这个线程的其他地方有很多有趣的讨论。基本上,我们开始比较这个函数的两个版本,我将用以下愚蠢的名字来描述它:

  1. “zip-py”版本:

    def pairs(xs):
        for p in zip(xs[:-1], xs[1:]): 
            yield p
    
  2. “循环”版本:

    def pairs(xs):
        last = object()
        dummy = last
        for x in xs:
            if last is not dummy:
                yield last,x
            last = x
    

那么为什么 loopy 版本会变慢呢?基本上,我认为这归结为几件事:

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

    • @mambocab 的编辑表明,不进行此比较确实会使版本变得循环
      稍微快一点,但并没有完全缩小差距。

  2. zippy 版本在编译的 C 代码中比 loopy 版本在 Python 代码中的作用更多:

    • 将两个对象组合成一个tuple。 loopy 版本是yield last,x,而在zippy 版本中,元组p 直接来自zip,所以它只是yield p。

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

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

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

【讨论】:

  • 有趣的事实:在可能的情况下,zip 的 Python 3 迭代器实现实际上是 reuses the tuple yielded at each step of iteration,以避免每次都分配一个新的元组。我在这个问题中使用 Python 2 进行基准测试,所以这个元组重用与答案没有直接关系。不过还是很有趣!
【解决方案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

【讨论】:

  • 您的pairsLoop 版本是什么?我认为zip 和izip 版本之间应该没有显着差异,只要整个列表可以轻松放入内存即可。可能的罪魁祸首是 OP 的循环版本在每个循环中都做了额外的工作,如 I wrote above。
  • 我已更新帖子以包含所有内容。我没有更改 OP 中的任何代码。
  • 您正在运行与 OP 略有不同的测试:您正在使用一个简短列表 (range(1,6)) 进行多次 (1,000,000) 次迭代,而 OP 正在执行几次 (100) 次迭代一长串(range(2**15))。这可能会有所作为......
【解决方案3】:

我怀疑第二个版本较慢的主要原因是因为它对它yields 的每一对都进行了比较操作:

# 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]

【讨论】:

  • 我认为我没有很清楚地传达它,但我在我的问题中提到我在没有比较的情况下尝试了它(使输出不正确)。时间安排在我问题的最底部——似乎比较是运行时的一个很好的部分,但它仍然比切片和压缩方法慢。
  • 我无法按照我在此评论中的要求格式化输出,因此我将对您提出的实施方案添加到问题本身的评估中。
  • 循环版本隐含的另一个“额外工作”是将两个单独的对象组合成一个tuple (yield last,x)。 zip 版本在编译的 C 代码中而不是在 Python 中执行此操作......这也可能会稍微加快速度。
  • 您的 cmets 总结为“好吧,zip/izip 处理 C 中的列表代码,这可能就是它们更快的原因”。这对我来说已经足够了。如果您编译一个答案,指出循环中(可能)优化掉的操作,我会接受它。
猜你喜欢
  • 1970-01-01
  • 2012-06-29
  • 2014-02-19
  • 2015-03-26
  • 2023-03-05
  • 1970-01-01
  • 1970-01-01
  • 2019-01-31
  • 1970-01-01
相关资源
最近更新 更多