Usar la insert
funció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 a
comienza 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.
python
performance
Desbordamiento de montón
fuente
fuente
a=[]; a[0:0]=[0]
hace lo mismo quea=[]; a[100:200]=[0]
a=[1,2,3];a[100:200]=[4]
se agrega4
al final de la listaa
interesante.a=[]; a[0:0]=[0]
o quea[0:0]=[0]
hace lo mismo quea[100:200]=[0]
...Respuestas:
Creo que es probablemente sólo que se olvidaron de usar
memmove
enlist.insert
. Si observa el código quelist.insert
usa para cambiar elementos, puede ver que es solo un bucle manual:mientras que
list.__setitem__
en la ruta de asignación de corte utilizamemmove
:memmove
normalmente tiene mucha optimización, como aprovechar las instrucciones SSE / AVX.fuente
-O3
la 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 realmemmove
, 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
)memmove
llamada real , entonces puede aprovechar todas las extensiones presentes en el momento de la ejecución?cpuid
). Lo mismo para varias otras funciones mem / str. Por lo tanto, las distribuciones pueden compilarse solo-O2
para 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.)memmove
versiones 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