¿Por qué a.insert (0,0) es mucho más lento que a [0: 0] = [0]?

61

Usar la insertfunción de una lista es mucho más lento que lograr el mismo efecto usando la asignación de corte:

> 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

(Tenga en cuenta que a=[]solo es la configuración, por lo que acomienza vacío pero luego crece hasta 100,000 elementos).

Al principio pensé que tal vez sea la búsqueda de atributos o la sobrecarga de llamadas a funciones, pero insertar cerca del final muestra que eso es insignificante:

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

¿Por qué la función de "insertar un solo elemento" presumiblemente más simple es mucho más lenta?

También puedo reproducirlo en 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)

Yo uso Python 3.8.1 de 32 bits en Windows 10 de 64 bits.
repl.it usa Python 3.8.1 de 64 bits en Linux de 64 bits.

Desbordamiento de montón
fuente
Es interesante notar que a=[]; a[0:0]=[0]hace lo mismo quea=[]; a[100:200]=[0]
smac89
¿Hay alguna razón por la que esté probando esto con solo una lista vacía?
MisterMiyagi
@MisterMiyagi Bueno, tengo que comenzar con algo . Tenga en cuenta que está vacío solo antes de la primera inserción y crece hasta 100,000 elementos durante el punto de referencia.
Desbordamiento del montón
@ smac89 a=[1,2,3];a[100:200]=[4]se agrega 4al final de la lista ainteresante.
Ch3steR
1
@ smac89 Si bien eso es cierto, en realidad no tiene que ver con la pregunta y me temo que podría inducir a error a alguien a pensar que estoy haciendo una evaluación comparativa a=[]; a[0:0]=[0]o que a[0:0]=[0]hace lo mismo que a[100:200]=[0]...
Heap Overflow

Respuestas:

57

Creo que es probablemente sólo que se olvidaron de usar memmoveen list.insert. Si observa el código que list.insert usa para cambiar elementos, puede ver que es solo un bucle manual:

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

mientras que list.__setitem__en la ruta de asignación de corte utilizamemmove :

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

memmove normalmente tiene mucha optimización, como aprovechar las instrucciones SSE / AVX.

user2357112 es compatible con Monica
fuente
55
Gracias. Creó un problema que hace referencia a esto.
Desbordamiento del montón
77
Si el intérprete se creó con -O3la vectorización automática habilitada, ese bucle manual podría compilarse de manera eficiente. Pero a menos que el compilador reconozca el bucle como un movimiento de memoria y lo compile en una llamada real memmove, solo puede aprovechar las extensiones del conjunto de instrucciones habilitadas en el momento de la compilación. (Bien si está construyendo el suyo propio -march=native, no tanto para los binarios de distribución construidos con la línea de base). Y GCC no desenrollará los bucles de forma predeterminada a menos que use PGO ( -fprofile-generate/ run / ...-use)
Peter Cordes
@PeterCordes ¿Le entiendo correctamente que si el compilador lo compila en una memmovellamada real , entonces puede aprovechar todas las extensiones presentes en el momento de la ejecución?
Desbordamiento de
1
@HeapOverflow: Sí. En GNU / Linux, por ejemplo, glibc sobrecarga la resolución del símbolo del enlazador dinámico con una función que selecciona la mejor versión asm escrita a mano de memmove para esta máquina en función de los resultados de detección de CPU guardados. (por ejemplo, en x86, se usa una función de inicio glibc cpuid). Lo mismo para varias otras funciones mem / str. Por lo tanto, las distribuciones pueden compilarse solo -O2para hacer binarios de ejecución en cualquier lugar, pero al menos haga que memcpy / memmove use un bucle AVX desenrollado que carga / almacena 32 bytes por instrucción. (O incluso AVX512 en las pocas CPU donde es una buena idea; creo que solo Xeon Phi.)
Peter Cordes
1
@HeapOverflow: No, hay varias memmoveversiones en libc.so, la biblioteca compartida. Para cada función, el despacho ocurre una vez, durante la resolución del símbolo (enlace temprano o en la primera llamada con enlace perezoso tradicional). Como dije, simplemente sobrecarga / engancha cómo ocurre la vinculación dinámica, no envolviendo la función en sí. (específicamente a través del mecanismo ifunc de GCC: code.woboq.org/userspace/glibc/sysdeps/x86_64/multiarch/… ). Relacionado: para memset, la opción habitual en las CPU modernas es __memset_avx2_unaligned_erms ver estas preguntas y respuestas
Peter Cordes,