繁体   English   中英

为什么这个生成器管道比 Python 中的传统循环慢?

[英]Why is this generator pipeline slower than a traditional loop in Python?

我做了一个传统的循环,并将其转换为生成器函数的管道,然后使用 timeit 比较速度。 令我惊讶的是,我发现生成器管道比传统循环慢了大约 25%。 我很好奇为什么生成器管道更慢。

这是传统的循环:

def process(numbers):
    results = []
    for number in numbers:
        if number < 0:
            continue
        if number % 2 != 0:
            continue
        multiplication = number * 3
        results.append(multiplication)
    return results

以下是将相同的循环分解为一系列生成器函数:

def positive(numbers):
    for number in numbers:
        if number >= 0:
            yield number


def even(numbers):
    for number in numbers:
        if number % 2 == 0:
            yield number


def multiply(numbers):
    for number in numbers:
        yield number * 3


def process2(numbers):
    return list(multiply(even(positive(numbers))))

他们都有相同的结果:

assert process(range(1000000)) == process2(range(1000000))

然而,传统的循环比生成器管道快大约 1/4:

import timeit

# Traditional:
setup = 'from __main__ import process'
print(timeit.timeit(stmt='process(range(1000000))', setup=setup, number=100))
# 12.314972606060678

# Generator pipeline
setup2 = 'from __main__ import process2'
print(timeit.timeit(stmt='process2(range(1000000))', setup=setup2, number=100))
# 16.349763878787826

我本来预计这两个速度大致相似。

导致速度降低的生成器管道是什么?

要了解为什么某事变慢或变快,请始终拉出分析器。 Python 自带几个:

import cProfile

>>> cProfile.run('process(range(900000))')
         450004 function calls in 0.171 seconds

   Ordered by: standard name

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.135    0.135    0.165    0.165 <stdin>:1(process)
        1    0.005    0.005    0.171    0.171 <string>:1(<module>)
        1    0.000    0.000    0.171    0.171 {built-in method builtins.exec}
   450000    0.031    0.000    0.031    0.000 {method 'append' of 'list' objects}
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}


>>> cProfile.run('process2(range(900000))')
         1800007 function calls in 0.356 seconds

   Ordered by: standard name

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
   450001    0.140    0.000    0.238    0.000 <stdin>:1(even)
   900001    0.098    0.000    0.098    0.000 <stdin>:1(multiply)
   450001    0.072    0.000    0.310    0.000 <stdin>:1(positive)
        1    0.040    0.040    0.351    0.351 <stdin>:1(process2)
        1    0.006    0.006    0.356    0.356 <string>:1(<module>)
        1    0.000    0.000    0.356    0.356 {built-in method builtins.exec}
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}

请注意,似乎 function multiply对列表中的每个项目都调用一次。 起初我认为这很荒谬,但当您考虑生成器的工作原理时 - 这是有道理的。 当您yield时,上下文必须暂停,然后当执行继续时 - 就好像您重新进入该函数的上下文一样。 分析器将此视为对 function 的调用是有道理的。 您可以在tottime列中查看每个 function 使用了多少时间。

所以最后 - 答案类似于您在评论中得到的答案 - 生成器和yield关键字比等效但纯粹的程序代码花费更多时间。

因为你增加了一堆开销来停止和恢复这些生成器。 生成器非常高效(多次启动和停止它们只花费你 50 毫秒,这对我来说非常惊人),但它们是 CPU 和 RAM 之间的基本权衡。 为开销放弃几个 CPU 周期,作为回报,在给定时刻 memory 中只有一个值。 除非将 memory 分配给非常大的对象或其他类似的东西会支配您的 CPU 使用率,否则您通常不会通过使用生成器而不是列表/循环来获得运行时性能。

但是,正如@modesitt 在他们的评论中指出的那样,使用列表推导将比上面的所有示例都快,因为您演示的案例非常简单。 如果您希望最终结果全部存储在 memory 中,请考虑使用列表推导而不是生成器推导。 您将获得推导式的轻微性能优势,而无需生成器的额外开销。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM