为何sum结合列表推导式比生成器表达式更快?附性能测试数据
这是个非常有意思的观察!我来帮你拆解一下背后的原因——核心在于两者在Python内部的执行机制差异,尤其是字节码层面的操作、迭代开销以及缓存效率的区别。
1. 字节码执行流程的本质差异
我们可以用Python的dis模块直接查看两种写法的字节码,就能直观看到区别:
对于列表推导式 sum([ch in A for ch in B]),字节码会先执行BUILD_LIST指令,一次性把所有ch in A的布尔结果打包成一个列表,再把这个列表传给sum函数去遍历求和。
而生成器表达式 sum(ch in A for ch in B),字节码会先执行GEN_EXPR指令创建一个生成器对象,然后sum函数每次都要通过NEXT指令去生成器里“取”下一个元素。这每一次NEXT操作都相当于一次函数调用,带来了额外的开销。
举个简化的字节码对比(截取关键部分):
- 列表推导式关键字节码:
BUILD_LIST 0 LOAD_FAST 0 (B) GET_ITER FOR_ITER 12 (to 18) ...(计算ch in A) LIST_APPEND 2 JUMP_ABSOLUTE 6 LOAD_NAME 2 (sum) CALL_FUNCTION 1 - 生成器表达式关键字节码:
LOAD_NAME 2 (sum) LOAD_CONST 0 (<code object <genexpr> at ...>) MAKE_FUNCTION 0 LOAD_FAST 0 (B) GET_ITER CALL_FUNCTION 1 CALL_FUNCTION 1
2. 迭代器的函数调用开销
生成器本质是一个迭代器,每次获取下一个元素都要调用它的__next__()方法。这个方法调用本身就有额外的开销:比如创建栈帧、处理StopIteration异常、销毁栈帧等等。
而列表是一个直接的序列对象,sum遍历列表时,直接通过索引访问内存里的元素,不需要每次都触发函数调用,自然省去了这部分开销。
3. CPU缓存的局部性优势
列表推导式生成的列表是一块连续的内存空间,CPU在访问时可以利用缓存局部性原理——一次性把相邻的多个元素加载到CPU缓存中,后续访问时直接从缓存读取,速度远快于从主存读取。
而生成器是逐个计算并返回元素,没有连续内存的特性,每次获取元素都可能需要从主存读取,缓存命中率低,这也会拖慢整体速度。
从你的测试数据看趋势
你提供的测试结果正好印证了这些差异:当B的字符数从10增加到10万时,两者的时间差从17µs(112-94.6)扩大到68ms(908-840)。这是因为迭代器的开销是累积的,数据量越大,额外的函数调用和缓存低效带来的影响就越明显。
小补充:时间与空间的权衡
不过要注意,这种速度优势是有代价的:列表推导式会一次性把所有结果存入内存,如果B的规模特别大(比如百万级以上),列表会占用大量内存,甚至导致内存不足。这时候生成器的内存优势就会凸显——它不会一次性生成所有元素,而是按需计算,内存占用极低。所以实际使用中要根据场景选择:追求速度且数据量不大时用列表推导式,追求内存效率时用生成器表达式。
内容的提问来源于stack exchange,提问作者Chris94549

