Dlaczego a.insert (0,0) jest znacznie wolniejszy niż [0: 0] = [0]?

61

Korzystanie z insertfunkcji listy jest znacznie wolniejsze niż uzyskiwanie tego samego efektu za pomocą przypisania plasterka:

> 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

(Pamiętaj, że a=[]to tylko konfiguracja, więc azaczyna się puste, ale potem rośnie do 100 000 elementów.)

Na początku myślałem, że może to narzut związany z wyszukiwaniem atrybutów lub wywołaniem funkcji, ale wstawianie pod koniec pokazuje, że jest to nieistotne:

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

Dlaczego przypuszczalnie prostsza funkcja „wstaw pojedynczy element” jest o wiele wolniejsza?

Mogę również odtworzyć go na 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)

Używam 32-bitowego języka Python 3.8.1 w systemie Windows 10 64-bitowym.
repl.it używa 64-bitowego języka Python w wersji 64-bitowej.

Przepełnienie sterty
źródło
Warto zauważyć, że a=[]; a[0:0]=[0]robi to samo coa=[]; a[100:200]=[0]
smac89
Czy jest jakiś powód, dla którego testujesz to z pustą listą?
MisterMiyagi,
@MisterMiyagi Cóż, muszę zacząć od czegoś . Pamiętaj, że jest pusty tylko przed pierwszym wstawieniem i rośnie do 100 000 elementów podczas testu porównawczego.
Przepełnienie stosu
@ smac89 a=[1,2,3];a[100:200]=[4]jest dodanie 4na końcu listy ainteresujące.
Ch3steR
1
@ smac89 To prawda, ale tak naprawdę nie ma to związku z pytaniem i obawiam się, że może to wprowadzić w błąd kogoś, kto pomyśli, że przeprowadzam testy porównawcze a=[]; a[0:0]=[0]lub a[0:0]=[0]robi to samo, co a[100:200]=[0]...
Przepełnienie stosu

Odpowiedzi:

57

Myślę, że to prawdopodobnie tylko, że zapomniał użyć memmovew list.insert. Jeśli przyjrzysz się kodowi list.insert używanemu do przenoszenia elementów, zobaczysz, że jest to po prostu ręczna pętla:

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

podczas gdy list.__setitem__na ścieżce przypisania plasterka używamemmove :

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

memmove zazwyczaj zawiera wiele optymalizacji, takich jak wykorzystanie instrukcji SSE / AVX.

user2357112 obsługuje Monikę
źródło
5
Dzięki. Utworzono problem odwołujący się do tego.
Przepełnienie stosu
7
Jeśli interpreter został zbudowany z -O3włączoną automatyczną wektoryzacją, ta ręczna pętla mogłaby się wydajnie skompilować. Ale dopóki kompilator nie rozpozna pętli jako memmove i nie skompiluje jej w rzeczywiste wywołanie memmove, może skorzystać z rozszerzeń zestawu instrukcji włączonych w czasie kompilacji. (Dobrze, jeśli budujesz własny -march=native, nie tyle w przypadku binariów dystrybucji zbudowanych z linii bazowej). I GCC domyślnie nie rozwija pętli, chyba że użyjesz PGO ( -fprofile-generate/ run / ...-use)
Peter Cordes
@PeterCordes Czy dobrze rozumiem, że jeśli kompilator skompiluje to w rzeczywiste memmovewywołanie, to może wtedy skorzystać ze wszystkich rozszerzeń obecnych w czasie wykonywania?
Przepełnienie stosu
1
@HeapOverflow: Tak. Na przykład w GNU / Linux glibc przeciąża dynamiczną rozdzielczość symbolu linkera funkcją, która wybiera najlepszą odręczną wersję asm memmove dla tego komputera na podstawie zapisanych wyników wykrywania procesora. (np. na x86 używana jest funkcja init glibc cpuid). To samo dotyczy kilku innych funkcji mem / str. Więc dystrybucje mogą się kompilować tylko -O2po to, aby tworzyć pliki binarne działające w dowolnym miejscu, ale przynajmniej niech memcpy / memmove używa rozwijanej pętli AVX do ładowania / przechowywania 32 bajtów na instrukcję. (Lub nawet AVX512 na kilku procesorach, gdzie to dobry pomysł; myślę, że tylko Xeon Phi.)
Peter Cordes
1
@HeapOverflow: Nie, kilka memmovewersji znajduje się tam w bibliotece współdzielonej libc.so. Dla każdej funkcji wysyłanie odbywa się raz, podczas rozpoznawania symboli (wczesne wiązanie lub pierwsze wywołanie z tradycyjnym wiązaniem opóźnionym). Tak jak powiedziałem, po prostu przeciąża / przechwytuje dynamiczne łączenie, a nie zawija samą funkcję. (w szczególności za pomocą mechanizmu ifunc GCC: code.woboq.org/userspace/glibc/sysdeps/x86_64/multiarch/... ). Powiązane: jeśli chodzi o memset, zwykłym wyborem na nowoczesnych procesorach jest __memset_avx2_unaligned_erms to pytanie i odpowiedzi
Peter Cordes