逐x86指令剖析Python代码 探查列表乘整数操作底层实现
Python列表与整数相乘的执行逻辑及深度调试方法
近期在讨论Python解释器执行列表乘整数操作(例如[1] * 3)的运行逻辑时,有观点认为Python会先生成3份[1]的副本再拼接,且列表推导式(例如[1 for _ in range(3)])效率更高、能规避额外开销。这个说法看似合理,但实际测试结果完全相反。
性能测试结果
测试环境:Windows平台 Python 3.9.7,使用timeit做100次重复执行,创建长度为1000000的列表:
>>> timeit.timeit('[1] * 1000000', number=100) 0.6567943999999954 >>> timeit.timeit('[1 for _ in range(1000000)]', number=100) 6.787221699999975
测试结果显示:列表乘法的运行速度比列表推导式快一个数量级。
通过dis模块反汇编对应函数只能看到BINARY_MULTIPLY字节码,无法看到操作的具体执行流程:
>>> def array_multiply(): ... return [1] * 3 ... >>> import dis >>> dis.dis(array_multiply) 2 0 LOAD_CONST 1 (1) 2 BUILD_LIST 1 4 LOAD_CONST 2 (3) 6 BINARY_MULTIPLY 8 RETURN_VALUE
核心结论与解答
首先纠正之前的错误认知:“列表乘整数会生成多份原列表副本再拼接”的说法完全不符合CPython的实际实现,这也是列表乘法性能远高于列表推导式的根本原因。
- 列表乘整数的C层实现不会做反复拷贝、拼接子列表的操作:执行时会直接计算最终列表的总长度,一次性申请对应大小的连续内存,再把原列表中的元素引用循环填充到新内存中,全程没有多余的内存重分配、对象拷贝开销。
- 列表推导式慢的原因是它需要完整走完迭代流程:逐次遍历range对象、每轮迭代创建整数引用、追加到列表中,过程中还会触发多次列表动态扩容,自然性能差很多。
- 这个实现逻辑也能解释常见的列表乘法坑:
[[]] * 3生成的三个子列表是同一个对象引用,修改其中一个会同步影响另外两个——因为实现根本没有拷贝子列表,只是复制了引用。
如果要做更深层次的执行流程分析,可以用下面两种手段:
1. 查看C层实现代码
CPython中列表乘整数的逻辑实现在列表对象的C源码中,对应3.9版本的listobject.c文件里的list_repeat函数,逻辑非常直白:
- 首先校验乘数为非负整数,若为负数直接返回空列表
- 计算新列表总长度 = 原列表长度 * 乘数,一次性分配对应大小的列表结构内存
- 遍历原列表存储的元素指针,按循环顺序填充到新列表的内存槽位中,全程没有生成原列表副本、拼接的操作。
2. 逐x86指令调试机器码执行
用原生调试工具即可完成,不需要特殊支持:
- 先获取对应Python 3.9.7版本的调试符号,或者自行编译带调试信息、关闭优化的CPython版本
- 用gdb或x64dbg附加到Python进程,在
list_repeat函数入口打端点,触发[1] * 3执行时就会断在对应逻辑处 - 之后就可以单步执行每一条x86指令,观察内存申请、引用填充的完整过程,和调试普通C程序没有区别。
补充提醒:列表乘法的性能优势仅适用于填充不可变对象、或者需要多个引用指向同一个对象的场景。如果需要生成独立的可变对象(比如多个独立空列表),必须使用列表推导式,避免引用绑定导致的逻辑错误。
内容的提问来源于stack exchange,提问作者klamann
相关产品推荐
相关产品推荐

