列表扩展:slice与islice的性能差异及困惑
为什么slice比islice在列表扩展场景中性能更优?
测试发现,理论上更具优势的islice,在列表扩展场景中实际性能反而不如slice。以下是性能对比的Python代码:
from time import perf_counter from itertools import islice from random import choices from string import ascii_letters import sys test_str = "".join(choices(ascii_letters, k=10 ** 7)) arr1 = [] t0 = perf_counter() for _ in range(10): arr1.extend(test_str[slice(1, sys.maxsize)]) t1 = perf_counter() print('%5.1f ms ' % ((t1 - t0) * 1e3)) arr2 = [] t0 = perf_counter() for _ in range(10): arr2.extend(islice(test_str, 1, sys.maxsize)) t1 = perf_counter() print('%5.1f ms ' % ((t1 - t0) * 1e3))
运行结果:
552.6 ms 786.0 ms
我对两者的执行逻辑理解如下:
严格切片(slice)
- 分配临时字符串
tmp,即原字符串从索引1到末尾的不可变字符串 - 获取
tmp的迭代器 - 为每个字符创建字符串对象
- 将这些对象添加到列表中
- 回收
tmp
延迟切片(islice)
- 获取
test_str的迭代器,丢弃第一个元素 - 为每个字符创建字符串对象
- 将这些对象添加到列表中
两者均由C语言实现,但我无法理解为何一次性分配临时字符串后回收的slice反而性能更优,特此寻求解答。
解答
核心原因在于内存访问效率与批量操作的底层优化:
字符串切片的零拷贝特性:Python字符串是不可变对象,
test_str[slice(1, sys.maxsize)]并不会复制原字符串的字符数据,只是创建一个新的字符串对象,共享原字符串的内存缓冲区,仅修改了起始偏移和长度参数。所谓的"临时字符串"只是一个轻量的包装,几乎没有额外内存开销。迭代器的开销差异:
- 基于切片的字符串迭代是针对连续内存块的专用实现,C层可以批量处理字符,CPU缓存命中率极高,迭代效率远高于通用迭代器。
islice作为通用迭代器适配器,每次迭代都要通过通用迭代协议执行next()调用,还要持续判断是否到达终止条件(sys.maxsize),这带来了额外的调用成本和分支判断开销,即使是C实现,这种通用逻辑也比字符串专用迭代慢。
extend的预分配优化:列表的
extend方法在处理序列类型(如字符串)时,能提前获取元素总数,一次性预分配足够的内存,减少内存扩容的次数。而islice是迭代器,extend无法提前知晓元素数量,只能逐步扩容,进一步增加了性能损耗。
内容的提问来源于stack exchange,提问作者Mathias Sven
相关产品推荐
相关产品推荐

