Why is summing list comprehension faster than generator expression?
不确定标题是否是正确的术语。
如果你必须比较2个字符串(A,B)中的字符并计算B中字符与A的匹配次数:
1 | sum([ch in A for ch in B]) |
在 %timeit 上比
快
1 | 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 毫秒
我查看了每个构造的反汇编(使用 dis)。我通过声明这两个函数来做到这一点:
1 2 3 4 5 | def list_comprehension(): return sum([ch in A for ch in B]) def generation_expression(): return sum(ch in A for ch in B) |
然后用每个函数调用
对于列表理解:
1 2 3 4 5 6 7 8 9 10 | 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 |
和生成器表达式:
1 2 3 4 5 6 7 8 9 10 11 | 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 |
实际求和的反汇编为:
1 2 3 4 5 6 7 8 9 | 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 |
但是这个
前两次反汇编之间的不同字节码指令是
我不会假装我知道 Python 字节码的内在原理,但我从中得出的结论是生成器表达式是作为队列实现的,在队列中生成值然后弹出。这种弹出不一定发生在列表推导式中,这让我相信使用生成器会产生少量开销。
现在这并不意味着生成器总是会变慢。生成器在内存效率方面表现出色,所以会有一个阈值 N,使得列表理解在这个阈值之前会表现得稍微好一些(因为内存使用不会成为问题),但是在这个阈值之后,生成器的表现会明显更好。
生成器通常比列表理解慢,生成器的重点是提高内存效率,因为它们通过以惰性方式(仅在实际需要时)创建每个项目来生成每个项目。他们更喜欢内存效率而不是速度。