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


61

使用列表的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开始为空,然后增长到100,000个元素。)

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

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

为什么大概更简单的专用“插入单个元素”功能这么慢?

我也可以在repl.it复制它:

from timeit import repeat

for _ in range(3):
  for stmt in 'a.insert(0,0)', 'a[0:0]=[0]', 'a.insert(-1,0)':
    t = min(repeat(stmt, 'a=[]', number=10**5))
    print('%.6f' % t, stmt)
  print()

# Example output:
#
# 4.803514 a.insert(0,0)
# 1.807832 a[0:0]=[0]
# 0.012533 a.insert(-1,0)
#
# 4.967313 a.insert(0,0)
# 1.821665 a[0:0]=[0]
# 0.012738 a.insert(-1,0)
#
# 5.694100 a.insert(0,0)
# 1.899940 a[0:0]=[0]
# 0.012664 a.insert(-1,0)

我在Windows 10 64位上使用32位Python 3.8.1。
repl.it在Linux 64位上使用Python 3.8.1 64位。


有趣的是,a=[]; a[0:0]=[0]它与a=[]; a[100:200]=[0]
smac89

有什么理由为什么只用一个空列表进行测试?
MiyaMiyagi

@MisterMiyagi好吧,我必须从一些开始。请注意,它仅在第一次插入之前为空,并在基准测试期间增长到100,000个元素。
堆溢出

@ smac89 a=[1,2,3];a[100:200]=[4]附加4到列表的结尾a很有趣。
Ch3steR

1
@ smac89虽然这是真实的,它并没有真正有问题的做,我担心这可能会误导人进入以为我标杆a=[]; a[0:0]=[0]a[0:0]=[0]不相同的a[100:200]=[0]...
堆溢出

Answers:


57

我认为它可能只是他们忘了使用memmovelist.insert。如果您看一下用于移位元素的代码 list.insert,您会发现它只是一个手动循环:

for (i = n; --i >= where; )
    items[i+1] = items[i];

list.__setitem__在切片分配路径上使用memmove

memmove(&item[ihigh+d], &item[ihigh],
    (k - ihigh)*sizeof(PyObject *));

memmove 通常会有很多优化,例如利用SSE / AVX指令。


5
谢谢。创建了一个与此相关的问题
堆溢出

7
如果解释器是在-O3启用了自动矢量化的情况下构建的,则该手动循环可能会有效地进行编译。但是除非编译器将循环识别为记忆体并将其编译为对的实际调用memmove,否则它只能利用在编译时启用的指令集扩展。(如果您使用来构建自己的-march=native,那就太好了,对于使用基线构建的发行版二进制文件则不那么重要)。而GCC不会展开循环默认情况下,除非您使用PGO(-fprofile-generate/运行/ ...-use
彼得·科德斯

@PeterCordes我是否正确理解您,如果编译器确实将其编译为实际memmove调用,那么可以利用执行时出现的所有扩展?
堆溢出

1
@HeapOverflow:是的。例如,在GNU / Linux上,glibc使用可根据保存的CPU检测结果为该计算机选择最佳手写记忆体版本的函数来使动态链接程序符号解析过载。(例如,在x86上,glibc初始化函数使用cpuid)。与其他几个mem / str函数相同。因此发行版可以仅通过编译就-O2可以运行任何位置的二进制文件,但是至少memcpy / memmove使用展开的AVX循环加载/存储每条指令32个字节。(甚至在少数几个CPU上使用AVX512也是个好主意;我认为只是Xeon Phi。)
Peter Cordes

1
@HeapOverflow:不,memmove共享库libc.so中有几个版本。对于每个函数,在符号解析期间(早期绑定或使用传统延迟绑定的第一次调用),调度都会发生一次。就像我说的那样,它只是重载/挂钩了动态链接的发生方式,而不是通过包装函数本身。(特别是通过GCC的ifunc机制:code.woboq.org/userspace/glibc/sysdeps/x86_64/multiarch/…)。相关:对于memset的现代的CPU通常的选择是__memset_avx2_unaligned_erms 看到这个Q&A
彼得·科德斯
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.