Warum ist a.insert (0,0) viel langsamer als a [0: 0] = [0]?

61

Die Verwendung der insertFunktion einer Liste ist viel langsamer als die Erzielung des gleichen Effekts mithilfe der Slice-Zuweisung:

> 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

(Beachten Sie, dass dies a=[]nur das Setup ist. aBeginnt also leer, wächst dann aber auf 100.000 Elemente.)

Zuerst dachte ich, dass es vielleicht die Attributsuche oder der Funktionsaufruf-Overhead oder so ist, aber das Einfügen gegen Ende zeigt, dass das vernachlässigbar ist:

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

Warum ist die vermutlich einfachere dedizierte Funktion "Einzelelement einfügen" so viel langsamer?

Ich kann es auch bei repl.it reproduzieren :

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)

Ich verwende Python 3.8.1 32-Bit unter Windows 10 64-Bit.
repl.it verwendet Python 3.8.1 64-Bit unter Linux 64-Bit.

Heap-Überlauf
quelle
Interessant zu bemerken, dass a=[]; a[0:0]=[0]das gleiche tut wiea=[]; a[100:200]=[0]
smac89
Gibt es einen Grund, warum Sie dies nur mit einer leeren Liste testen?
MisterMiyagi
@MisterMiyagi Nun, ich muss mit etwas anfangen . Beachten Sie, dass es erst vor dem ersten Einfügen leer ist und während des Benchmarks auf 100.000 Elemente anwächst.
Heap Overflow
@ smac89 a=[1,2,3];a[100:200]=[4]wird 4am Ende der Liste ainteressant angehängt .
Ch3steR
1
@ smac89 Das stimmt zwar, hat aber nicht wirklich mit der Frage zu tun, und ich befürchte, es könnte jemanden in die Irre führen, zu denken, dass ich ein Benchmarking durchführe a=[]; a[0:0]=[0]oder a[0:0]=[0]dasselbe wie a[100:200]=[0]...
Heap Overflow

Antworten:

57

Ich denke , es ist wahrscheinlich nur , dass sie vergessen haben , zu verwenden memmovein list.insert. Wenn Sie sich den Code ansehen, mit dem list.insertElemente verschoben werden, sehen Sie, dass es sich nur um eine manuelle Schleife handelt:

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

Während list.__setitem__auf dem Slice-Zuweisungspfad verwendet wirdmemmove :

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

memmove In der Regel werden viele Optimierungen vorgenommen, z. B. die Nutzung von SSE / AVX-Anweisungen.

user2357112 unterstützt Monica
quelle
5
Vielen Dank. Es wurde ein Problem erstellt, das darauf verweist.
Heap Overflow
7
Wenn der Interpreter mit -O3aktivierter automatischer Vektorisierung erstellt wurde, kann diese manuelle Schleife effizient kompiliert werden. Wenn der Compiler die Schleife jedoch nicht als memmove erkennt und zu einem tatsächlichen Aufruf von kompiliert memmove, kann er nur die zum Zeitpunkt der Kompilierung aktivierten Befehlssatzerweiterungen nutzen. (Gut, wenn Sie Ihre eigenen mit -march=nativeerstellen, nicht so sehr für Distribution-Binärdateien, die mit Baseline erstellt wurden). Und GCC rollt Schleifen standardmäßig nicht ab, es sei denn, Sie verwenden PGO ( -fprofile-generate/ run / ...-use)
Peter Cordes
@PeterCordes Verstehe ich Sie richtig, dass der Compiler, wenn er ihn zu einem tatsächlichen memmoveAufruf kompiliert , alle zur Ausführungszeit vorhandenen Erweiterungen nutzen kann?
Heap Overflow
1
@HeapOverflow: Ja. Unter GNU / Linux überlastet glibc beispielsweise die Auflösung dynamischer Linkersymbole mit einer Funktion, die die beste handgeschriebene asm-Version von memmove für diesen Computer basierend auf gespeicherten CPU-Erkennungsergebnissen auswählt. (zB auf x86 wird eine glibc init-Funktion verwendet cpuid). Gleiches gilt für mehrere andere mem / str-Funktionen. Distros können also nur kompiliert werden -O2, um Binärdateien zu erstellen , aber memcpy / memmove verwenden zumindest eine nicht gerollte AVX-Schleife, die 32 Bytes pro Befehl lädt / speichert. (Oder sogar AVX512 auf den wenigen CPUs, auf denen das eine gute Idee ist; ich denke nur Xeon Phi.)
Peter Cordes
1
@HeapOverflow: Nein, dort befinden sich mehrere memmoveVersionen in libc.so, der gemeinsam genutzten Bibliothek. Für jede Funktion erfolgt der Versand einmal während der Symbolauflösung (frühzeitige Bindung oder beim ersten Aufruf mit herkömmlicher verzögerter Bindung). Wie gesagt, es überlädt / hakt nur, wie dynamische Verknüpfungen stattfinden, nicht durch Umschließen der Funktion selbst. (speziell über den ifunc-Mechanismus von GCC: code.woboq.org/userspace/glibc/sysdeps/x86_64/multiarch/… ). Verwandte: Für Memset ist die übliche Wahl auf modernen CPUs __memset_avx2_unaligned_erms diese Q & A
Peter Cordes