Python 为什么.insert(0,0)比[0:0]=[0]慢得多?

Python 为什么.insert(0,0)比[0:0]=[0]慢得多?,python,performance,Python,Performance,使用列表的insert函数比使用切片分配实现相同效果要慢得多: > python -m timeit -n 100000 -s "a=[]" "a.insert(0,0)" 100000 loops, best of 5: 19.2 usec per loop > python -m timeit -n 100000 -s "a=[]" "a[0:0]=[0]" 100000 loops, best of 5: 6.78 usec per loop (请注意,a=[]只是设置,因

使用列表的
insert
函数比使用切片分配实现相同效果要慢得多:

> python -m timeit -n 100000 -s "a=[]" "a.insert(0,0)"
100000 loops, best of 5: 19.2 usec per loop

> python -m timeit -n 100000 -s "a=[]" "a[0:0]=[0]"
100000 loops, best of 5: 6.78 usec per loop
(请注意,
a=[]
只是设置,因此
a
开始为空,然后增加到100000个元素。)

起初我认为可能是属性查找或函数调用开销,但在末尾插入说明这是可以忽略的:

> python -m timeit -n 100000 -s "a=[]" "a.insert(-1,0)"
100000 loops, best of 5: 79.1 nsec per loop
为什么可能更简单的专用“插入单个元素”功能会慢得多?

我也可以复制它:

我在Windows 10上使用32位Python 3.8.1 64位。

repl.it在64位Linux上使用Python 3.8.1 64位。

我想可能是因为他们忘了在
列表中使用
memmove
。insert
。如果您查看
列表。insert
用于移动元素,您可以看到它只是一个手动循环:

for (i = n; --i >= where; )
    items[i+1] = items[i];
当切片分配路径上的
列表。\uuuuu setitem\uuuuuu
时:


memmove
通常会进行大量优化,例如利用SSE/AVX指令。

值得注意的是
a=[];[0:0]=[0]
的作用与
a=[]相同;[100:200]=[0]
你有什么理由只用一个空列表来测试这个吗?@MisterMiyagi嗯,我必须从一些东西开始。请注意,它仅在第一次插入之前为空,并在基准测试期间增长到100000个元素。@smac89
a=[1,2,3];a[100:200]=[4]
在列表的末尾添加了
4
a
很有趣。@smac89虽然这是真的,但与问题无关,我担心这可能会误导别人认为我在进行基准测试
a=[];[0:0]=[0]
a[0:0]=[0]
的作用与
a[100:200]=[0]
相同……谢谢。创建了一个引用此的循环。如果解释器是在启用了自动矢量化的情况下构建的,则手动循环可能会高效编译。但是,除非编译器将循环识别为memmove并将其编译为对
memmove
的实际调用,否则它只能利用编译时启用的指令集扩展。(如果您正在使用
-march=native
构建自己的发行版,就可以了,但对于使用baseline构建的发行版二进制文件来说就没有那么多了)。默认情况下,GCC不会展开循环,除非您使用PGO(
-fprofile generate
/run/
…-use
)@PeterCordes我是否正确理解,如果编译器将其编译为实际的
memmove
调用,那么就可以利用执行时存在的所有扩展?@HeapOverflow:Yes。例如,在GNU/Linux上,glibc使用一个函数重载动态链接器符号解析,该函数根据保存的CPU检测结果为这台机器选择最好的手工编写的asm版本的memmove。(例如,在x86上,glibc init函数使用
cpuid
)。其他几个mem/str函数也是如此。因此,发行版可以只使用
-O2
进行编译,以使run-anywhere二进制文件运行,但至少要让memcpy/memmove使用一个展开的AVX循环,每个指令加载/存储32个字节。(甚至是在少数几个CPU上的AVX512,这是个好主意;我认为只有Xeon Phi。)@HeapOverflow:No,libc中有几个
memmove
版本。对于每个函数,在符号解析期间(早期绑定或使用传统延迟绑定的第一次调用时),分派只发生一次。正如我所说,它只是重载/挂钩动态链接的发生方式,而不是包装函数本身。(特别是通过GCC的ifunc机制:)。相关:对于memset,现代CPU上的常见选择是
\uuuuMemset\uAVX2\uUnaligned\uERMS
for (i = n; --i >= where; )
    items[i+1] = items[i];
memmove(&item[ihigh+d], &item[ihigh],
    (k - ihigh)*sizeof(PyObject *));