Korzystanie z insert
funkcji 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 a
zaczyna 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.
python
performance
Przepełnienie sterty
źródło
źródło
a=[]; a[0:0]=[0]
robi to samo coa=[]; a[100:200]=[0]
a=[1,2,3];a[100:200]=[4]
jest dodanie4
na końcu listya
interesujące.a=[]; a[0:0]=[0]
luba[0:0]=[0]
robi to samo, coa[100:200]=[0]
...Odpowiedzi:
Myślę, że to prawdopodobnie tylko, że zapomniał użyć
memmove
wlist.insert
. Jeśli przyjrzysz się kodowilist.insert
używanemu do przenoszenia elementów, zobaczysz, że jest to po prostu ręczna pętla:podczas gdy
list.__setitem__
na ścieżce przypisania plasterka używamemmove
:memmove
zazwyczaj zawiera wiele optymalizacji, takich jak wykorzystanie instrukcji SSE / AVX.źródło
-O3
włą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łaniememmove
, 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
)memmove
wywołanie, to może wtedy skorzystać ze wszystkich rozszerzeń obecnych w czasie wykonywania?cpuid
). To samo dotyczy kilku innych funkcji mem / str. Więc dystrybucje mogą się kompilować tylko-O2
po 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.)memmove
wersji 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