【问题标题】:Why is summing list comprehension faster than generator expression?为什么求和列表理解比生成器表达式更快?
【发布时间】:2020-11-08 12:29:25
【问题描述】:

不确定标题是否是正确的术语。

如果您必须比较 2 个字符串 (A,B) 中的字符并计算 B 中的字符与 A 的匹配数:

sum([ch in A for ch in B])

在 %timeit 上比

sum(ch in A for ch in B)

我知道第一个将创建一个 bool 列表,然后将 1 的值相加。 第二个是发电机。我不清楚它在内部做什么以及为什么速度较慢?

谢谢。

使用 %timeit 结果编辑:

10 个字符

生成器表达式

列表

10000 次循环,最好的 3 次:每个循环 112 µs

10000 次循环,3 次中的最佳:每个循环 94.6 µs

1000 个字符

生成器表达式

列表

100 个循环,3 个循环中的最佳值:每个循环 8.5 毫秒

100 个循环,3 个循环中的最佳值:每个循环 6.9 毫秒

10,000 个字符

生成器表达式

列表

10 个循环,3 个循环中的最佳值:每个循环 87.5 毫秒

10 个循环,3 个循环中的最佳:每个循环 76.1 毫秒

100,000 个字符

生成器表达式

列表

1 次循环,3 次取胜:每个循环 908 毫秒

1 个循环,3 个循环中的最佳值:每个循环 840 毫秒

【问题讨论】:

  • 字符串有多长?生成器是数据惰性的,这可能会增加一些开销,短字符串的开销会大于不复制数据所保存的开销。
  • 因为python非常非常擅长创建列表。
  • 嗯,即使是长度为10000000的字符串,列表版比生成器版快……我也很好奇速度差异。
  • 渐近地,它们似乎正在收敛。最终,必须进行两次遍历并构建列表的成本可能会超过更昂贵的生成器遍历。

标签: python python-3.x


【解决方案1】:

我查看了每个构造的反汇编(使用dis)。我通过声明这两个函数来做到这一点:

def list_comprehension():
    return sum([ch in A for ch in B])

def generation_expression():
    return sum(ch in A for ch in B)

然后用每个函数调用dis.dis

对于列表理解:

 0 BUILD_LIST               0
 2 LOAD_FAST                0 (.0)
 4 FOR_ITER                12 (to 18)
 6 STORE_FAST               1 (ch)
 8 LOAD_FAST                1 (ch)
10 LOAD_GLOBAL              0 (A)
12 COMPARE_OP               6 (in)
14 LIST_APPEND              2
16 JUMP_ABSOLUTE            4
18 RETURN_VALUE

对于生成器表达式:

 0 LOAD_FAST                0 (.0)
 2 FOR_ITER                14 (to 18)
 4 STORE_FAST               1 (ch)
 6 LOAD_FAST                1 (ch)
 8 LOAD_GLOBAL              0 (A)
10 COMPARE_OP               6 (in)
12 YIELD_VALUE
14 POP_TOP
16 JUMP_ABSOLUTE            2
18 LOAD_CONST               0 (None)
20 RETURN_VALUE

实际求和的反汇编是:

 0 LOAD_GLOBAL              0 (sum)
 2 LOAD_CONST               1 (<code object <genexpr> at 0x7f49dc395240, file "/home/mishac/dev/python/kintsugi/KintsugiModels/automated_tests/a.py", line 12>)
 4 LOAD_CONST               2 ('generation_expression.<locals>.<genexpr>')
 6 MAKE_FUNCTION            0
 8 LOAD_GLOBAL              1 (B)
10 GET_ITER
12 CALL_FUNCTION            1
14 CALL_FUNCTION            1
16 RETURN_VALUE

但是你的两个示例之间的 sum 反汇编是不变的,唯一的区别是 generation_expression.&lt;locals&gt;.&lt;genexpr&gt;list_comprehension.&lt;locals&gt;.&lt;listcomp&gt; 的加载(所以只是加载不同的局部变量)。

前两次反汇编之间的不同字节码指令是 LIST_APPEND 用于列表推导与 YIELD_VALUEPOP_TOP 的连接用于生成器表达式。

我不会假装我知道 Python 字节码的内在特性,但我从中了解到的是生成器表达式是作为一个队列实现的,其中值被生成然后弹出。这种弹出不一定发生在列表理解中,这让我相信使用生成器会产生少量开销。

现在这并不意味着生成器总是会变慢。生成器在内存效率方面表现出色,因此会有一个阈值 N 使得列表推导在此阈值之前表现会稍好(因为内存使用不会成为问题),但在此阈值之后,生成器将显着 表现更好。

【讨论】:

  • 您能否展示一下您用于获取字节码指令的代码?我也尝试过,但得到了不同的指示。
  • @jakub 是的,将其添加到答案中。
  • 谢谢,我现在得到了相同的结果。很好的答案
  • 另外,sum 是用 C 语言编写的,它使用列表迭代器的 tp_next 函数,该函数不必对每个值都执行 yield/pop 操作代码。我认为额外的开销在 YIELD 中。
  • 生成器是否会产生一些然后被求和的东西,就像列表推导产生布尔列表的方式一样?
【解决方案2】:

生成器通常比列表理解慢,生成器的全部意义在于内存效率,因为它们通过以惰性方式创建每个项目(仅在实际需要时)来生成每个项目。他们更喜欢内存效率而不是速度。

【讨论】:

  • 我试图理解这在这种情况下意味着什么。 sum(list comprehension) 首先生成一个 bool 列表,然后对 1 求和。生成器表达式有什么不同?
  • 在我对生成器的理解中,它们需要多个 next 调用,这可能(但并非总是)使它们变慢,请参见此处:生成器使用 next 调用,如您在此处看到的:python.org/dev/peps/pep-0289/#the-details 所以更长的迭代像字符串会导致更多的下一个调用,这会减慢速度。
  • 我找到了一个 SO 答案,详细解释了它:stackoverflow.com/questions/11964130/…
猜你喜欢
  • 2013-04-24
  • 1970-01-01
  • 2023-02-02
  • 2019-09-13
  • 2018-09-12
  • 2015-09-29
  • 2017-01-23
  • 2020-12-01
  • 2021-11-12
相关资源
最近更新 更多