为什么汇总列表理解比生成器表达式更快?

Chr*_*549 12 python python-3.x

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

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

sum([ch in A for ch in B])
Run Code Online (Sandbox Code Playgroud)

在 %timeit 上比

sum(ch in A for ch in B)
Run Code Online (Sandbox Code Playgroud)

我知道第一个将创建一个 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 毫秒

Mar*_*hac 10

我查看了每个构造的反汇编(使用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)
Run Code Online (Sandbox Code Playgroud)

然后调用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
Run Code Online (Sandbox Code Playgroud)

对于生成器表达式:

 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
Run Code Online (Sandbox Code Playgroud)

实际求和的反汇编是:

 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
Run Code Online (Sandbox Code Playgroud)

但是这个sum反汇编在你的两个例子之间是不变的,唯一的区别是generation_expression.<locals>.<genexpr>vs的加载list_comprehension.<locals>.<listcomp>(所以只加载一个不同的局部变量)。

前两个反汇编之间的不同字节码指令是LIST_APPEND用于列表理解对的结合YIELD_VALUEPOP_TOP用于发电机的表达。

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

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