def main():
for i in xrange(10**8):
pass
main()
Ten fragment kodu w Pythonie jest uruchamiany (Uwaga: synchronizacja odbywa się za pomocą funkcji czasu w BASH w systemie Linux).
real 0m1.841s
user 0m1.828s
sys 0m0.012s
Jeśli jednak pętla for nie jest umieszczona w funkcji,
for i in xrange(10**8):
pass
wtedy działa znacznie dłużej:
real 0m4.543s
user 0m4.524s
sys 0m0.012s
Dlaczego to?
python
performance
profiling
benchmarking
cpython
thedoctar
źródło
źródło
Odpowiedzi:
Możesz zapytać, dlaczego przechowywanie zmiennych lokalnych jest szybsze niż globals. Jest to szczegół implementacji CPython.
Pamiętaj, że CPython jest kompilowany do kodu bajtowego, który działa interpreter. Gdy funkcja jest kompilowana, zmienne lokalne są przechowywane w tablicy o stałym rozmiarze ( nie a
dict
), a nazwy zmiennych są przypisywane do indeksów. Jest to możliwe, ponieważ nie można dynamicznie dodawać zmiennych lokalnych do funkcji. Następnie pobranie zmiennej lokalnej jest dosłownie wyszukiwaniem wskaźnika na liście i wzrostem liczby rachunków,PyObject
co jest banalne.Porównaj to z globalnym wyszukiwaniem (
LOAD_GLOBAL
), które jest prawdziwymdict
wyszukiwaniem obejmującym skrót i tak dalej. Nawiasem mówiąc, dlatego musisz określić,global i
czy chcesz, aby była globalna: jeśli kiedykolwiek przypiszesz zmienną wewnątrz zakresu, kompilator wydaSTORE_FAST
s o dostęp, chyba że powiesz, żeby tego nie robiła.Nawiasem mówiąc, globalne wyszukiwania są nadal dość zoptymalizowane. Wyszukiwanie atrybutów
foo.bar
jest naprawdę powolne!Oto mała ilustracja na temat lokalnej zmiennej wydajności.
źródło
def foo_func: x = 5
,x
jest lokalny dla funkcji. Dostępx
jest lokalny.foo = SomeClass()
,foo.bar
to dostęp do atrybutów.val = 5
globalny jest globalny. Co do prędkości lokalnej> globalnej> atrybut zgodnie z tym, co tu przeczytałem. Dostępx
dofoo_func
jest więc najszybszy, następnie następujeval
, a następniefoo.bar
.foo.attr
nie jest przeglądem lokalnym, ponieważ w kontekście tego konwój mówimy o przeglądach lokalnych, które są wyszukiwaniem zmiennej należącej do funkcji.globals()
funkcję. Jeśli potrzebujesz więcej informacji, być może będziesz musiał zacząć szukać kodu źródłowego dla Pythona. CPython to tylko nazwa zwykłej implementacji Pythona - więc prawdopodobnie już go używasz!Wewnątrz funkcji kod bajtowy to:
Na najwyższym poziomie kod bajtowy to:
Różnica polega na tym, że
STORE_FAST
jest szybszy (!) NiżSTORE_NAME
. Jest tak, ponieważ w funkcjii
jest lokalny, ale na najwyższym poziomie jest globalny.Aby sprawdzić kod bajtowy, użyj
dis
modułu . Byłem w stanie zdemontować funkcję bezpośrednio, ale aby zdemontować kod najwyższego poziomu musiałem użyćcompile
wbudowanego .źródło
global i
domain
funkcji powoduje, że czasy działania są równoważne.locals()
,inspect.getframe()
itp.). Wyszukiwanie elementu tablicy za pomocą stałej liczby całkowitej jest znacznie szybsze niż wyszukiwanie słownika.Oprócz lokalnych / globalnych czasów przechowywania zmiennych, przewidywanie kodu operacyjnego przyspiesza działanie.
Jak wyjaśniają inne odpowiedzi, funkcja wykorzystuje
STORE_FAST
w pętli kod operacji. Oto kod bajtowy dla pętli funkcji:Zwykle po uruchomieniu programu Python wykonuje kolejno każdy kod operacji, śledząc stos i wykonując inne kontrole w ramce stosu po wykonaniu każdego kodu operacji. Prognozowanie kodu operacyjnego oznacza, że w niektórych przypadkach Python może przejść bezpośrednio do następnego kodu operacyjnego, unikając w ten sposób części tego narzutu.
W takim przypadku za każdym razem, gdy Python widzi
FOR_ITER
(górna część pętli), „przewiduje”, żeSTORE_FAST
jest to kolejny kod operacji, który musi wykonać. Python następnie zerknie na następny kod operacji, a jeśli prognoza była poprawna, skacze prosto doSTORE_FAST
. To powoduje ściśnięcie dwóch kodów operacyjnych w jeden kod operacji.Z drugiej strony
STORE_NAME
opcode jest używany w pętli na poziomie globalnym. Python * nie * robi podobnych prognoz, gdy widzi ten kod operacji. Zamiast tego musi wrócić do górnej części pętli oceny, co ma oczywiste implikacje dla prędkości, z jaką pętla jest wykonywana.Aby podać więcej szczegółów technicznych na temat tej optymalizacji, oto cytat z
ceval.c
pliku („silnik” maszyny wirtualnej Pythona):Widzimy w kodzie źródłowym
FOR_ITER
opcode dokładnie, gdzie dokonano prognozySTORE_FAST
:PREDICT
Funkcja rozszerza sięif (*next_instr == op) goto PRED_##op
, czyli po prostu przeskoczyć do początku przewidywanej opcode. W tym przypadku przeskakujemy tutaj:Zmienna lokalna jest teraz ustawiona i następny kod operacji jest gotowy do wykonania. Python kontynuuje iterowalność aż do końca, dzięki czemu za każdym razem dokonuje udanej prognozy.
Strona wiki Python zawiera więcej informacji na temat działania maszyny wirtualnej CPython.
źródło
HAS_ARG
test nigdy nie występuje (z wyjątkiem sytuacji, gdy śledzenie niskiego poziomu jest włączone zarówno podczas kompilacji, jak i w środowisku wykonawczym, czego nie robi żadna normalna wersja), pozostawiając tylko jeden nieprzewidywalny skok.PREDICT
makro jest całkowicie wyłączone; zamiast tego większość przypadków kończy sięDISPATCH
bezpośrednio na gałęzi. Ale w procesorach przewidujących rozgałęzienia efekt jest podobny do tegoPREDICT
, ponieważ rozgałęzianie (i przewidywanie) odbywa się dla poszczególnych kodów, co zwiększa szanse na pomyślne przewidywanie rozgałęzień.