为什么Python中all函数搭配列表推导比生成器表达式更快?
我正在研究Python的一些性能细节,重点关注all、any、max等内置函数中迭代器和生成器的使用情况。
先创建一个整数列表:
arr: list[int] = list(range(10_000))
随后测试了以下两种写法的执行耗时:
- 生成器表达式写法:
all(isinstance(x, int) for x in arr)
执行耗时:
1.04 ms ± 79.6 µs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)
- 列表推导式写法:
all([isinstance(x, int) for x in arr])
执行耗时:
915 µs ± 50.1 µs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)
我原本以为生成器表达式的写法会更快,因为它不需要额外创建数组占用内存,但实际结果却是列表推导式的写法更快。这是为什么?
为了探究原因,我对两个函数进行了反汇编,发现列表推导式对应的指令更少,这是不是关键原因?
反汇编代码如下:
In [11]: def f(): ...: return all(isinstance(x, int) for x in arr) ...: In [12]: def g(): ...: return all([isinstance(x, int) for x in arr]) ...: In [13]: from dis import dis In [14]: dis(f) ... Disassembly of <code object <genexpr> at 0x7f459e2537e0, file "<ipython-input-11-973ce3497c83>", line 2>: 0 GEN_START 0 2 2 LOAD_FAST 0 (.0) >> 4 FOR_ITER 8 (to 22) 6 STORE_FAST 1 (x) 8 LOAD_GLOBAL 0 (isinstance) 10 LOAD_FAST 1 (x) 12 LOAD_GLOBAL 1 (int) 14 CALL_FUNCTION 2 16 YIELD_VALUE 18 POP_TOP 20 JUMP_ABSOLUTE 2 (to 4) >> 22 LOAD_CONST 0 (None) 24 RETURN_VALUE In [15]: dis(g) ... Disassembly of <code object <listcomp> at 0x7f459ebf92c0, file "<ipython-input-12-d4d6130487f9>", line 2>: 2 0 BUILD_LIST 0 2 LOAD_FAST 0 (.0) >> 4 FOR_ITER 7 (to 20) 6 STORE_FAST 1 (x) 8 LOAD_GLOBAL 0 (isinstance) 10 LOAD_FAST 1 (x) 12 LOAD_GLOBAL 1 (int) 14 CALL_FUNCTION 2 16 LIST_APPEND 2 18 JUMP_ABSOLUTE 2 (to 4) >> 20 RETURN_VALUE
原因分析
1. 生成器的状态切换开销
生成器表达式每次迭代都要执行YIELD_VALUE指令,这个操作涉及生成器的状态切换——暂停当前执行、保存上下文,下次迭代再恢复。这种频繁的上下文切换会带来额外性能损耗,而列表推导式是一次性构建完整列表,没有这类开销。
2. 列表迭代的底层优化
Python对列表迭代有深度的底层优化,all()遍历列表时直接访问内存中的连续元素,效率远高于生成器每次生成下一个元素的过程。列表推导式的LIST_APPEND是高效的内存操作,而生成器的YIELD_VALUE需要处理更多解释器层面的逻辑。
3. 指令差异的累积效应
从反汇编结果看,生成器表达式多了GEN_START、POP_TOP指令,且FOR_ITER步长为8,列表推导式的FOR_ITER步长为7。单条指令的差异看似微小,但在一万次迭代的循环中,这些差异会被放大,最终导致整体耗时的差距。
4. 内存与性能的场景权衡
生成器的内存优势只在无需遍历全部元素的场景下体现——比如列表中存在不符合条件的元素,all()会提前终止迭代,这时生成器不需要构建完整列表,性能会优于列表推导式。但在这个测试场景中,所有元素都是int,all()必须遍历全部元素,内存节省的优势抵不过生成器的状态切换开销。
内容的提问来源于stack exchange,提问作者Alex Wignorbo

