Dlaczego Kod Pythona działa szybciej w funkcji?

def main():
    for i in xrange(10**8):
        pass
main()

Ten fragment kodu w Pythonie działa w (Uwaga: czas jest wykonywany za pomocą funkcji czasu w BASH w Linuksie.)

real    0m1.841s
user    0m1.828s
sys     0m0.012s

Jeśli jednak pętla for nie jest umieszczona wewnątrz funkcji,

for i in xrange(10**8):
    pass

Potem działa przez znacznie dłuższy czas:

real    0m4.543s
user    0m4.524s
sys     0m0.012s
Dlaczego tak jest?
Author: thedoctar, 2012-06-28

3 answers

Możesz zapytać dlaczego przechowywanie zmiennych lokalnych jest szybsze niż globalne. To jest szczegóły implementacji CPython.

Pamiętaj, że CPython jest kompilowany do kodu bajtowego, który jest uruchamiany przez 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 wyszukiwanie wskaźnika do listy i zwiększenie liczby refcount na PyObject, co jest trywialne.

Porównujemy to z globalnym wyszukiwaniem (LOAD_GLOBAL), które jest prawdziwym wyszukiwaniem dict zawierającym hash i tak dalej. Nawiasem mówiąc, dlatego musisz określić global i, Jeśli chcesz, aby była globalna: jeśli kiedykolwiek przypiszesz zmienną wewnątrz zakresu, kompilator wyda STORE_FAST s dla jej dostępu, chyba że każesz jej tego nie robić.

Przy okazji, globalne poszukiwania są nadal dość zoptymalizowane. Atrybut lookups foo.bar to naprawdę powolne!

Oto mała ilustracja {[23] } na temat lokalnej zmiennej wydajności.

 454
Author: Katriel,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-06-16 14:00:19

Wewnątrz funkcji kod bajtowy to

  2           0 SETUP_LOOP              20 (to 23)
              3 LOAD_GLOBAL              0 (xrange)
              6 LOAD_CONST               3 (100000000)
              9 CALL_FUNCTION            1
             12 GET_ITER            
        >>   13 FOR_ITER                 6 (to 22)
             16 STORE_FAST               0 (i)

  3          19 JUMP_ABSOLUTE           13
        >>   22 POP_BLOCK           
        >>   23 LOAD_CONST               0 (None)
             26 RETURN_VALUE        

Na najwyższym poziomie kod bajtowy to

  1           0 SETUP_LOOP              20 (to 23)
              3 LOAD_NAME                0 (xrange)
              6 LOAD_CONST               3 (100000000)
              9 CALL_FUNCTION            1
             12 GET_ITER            
        >>   13 FOR_ITER                 6 (to 22)
             16 STORE_NAME               1 (i)

  2          19 JUMP_ABSOLUTE           13
        >>   22 POP_BLOCK           
        >>   23 LOAD_CONST               2 (None)
             26 RETURN_VALUE        

Różnica polega na tym, że STORE_FAST jest szybszy (!) niż STORE_NAME. Dzieje się tak dlatego, że w funkcji i jest lokalna, ale na najwyższym poziomie jest globalna.

Aby sprawdzić bajt kodu, użyj dis Moduł . Udało mi się zdemontować funkcję bezpośrednio, ale aby zdemontować kod toplevel musiałem użyć compile builtin .

 634
Author: ecatmur,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-06-28 09:29:12

Oprócz czasu przechowywania zmiennych lokalnych/globalnych, przewidywanie kodu opcode sprawia, że funkcja jest szybsza.

Jak wyjaśniają Pozostałe odpowiedzi, funkcja wykorzystuje kod opcode STORE_FAST W pętli. Oto kod bajtowy pętli funkcji:

    >>   13 FOR_ITER                 6 (to 22)   # get next value from iterator
         16 STORE_FAST               0 (x)       # set local variable
         19 JUMP_ABSOLUTE           13           # back to FOR_ITER

Normalnie, gdy program jest uruchomiony, Python wykonuje każdy kod opcode jeden po drugim, śledząc stos a i preformując inne kontrole na ramce stosu po wykonaniu każdego kodu opcode. Opcode prediction oznacza, że w niektórych przypadki Python jest w stanie przeskoczyć bezpośrednio do następnego kodu, unikając w ten sposób niektórych kosztów.

W Tym Przypadku, za każdym razem, gdy Python zobaczy FOR_ITER (górną część pętli), "przewidzi", że STORE_FAST jest następnym kodem, który musi wykonać. Python następnie zagląda na następny kod opcode i, jeśli przewidywanie było poprawne, przeskakuje prosto do STORE_FAST. Powoduje to ściśnięcie dwóch kodów opcodes w jeden kod opcodes.

Z drugiej strony, kod STORE_NAME jest używany w pętli na globalnym poziom. Python *nie* tworzy podobne prognozy, gdy widzi ten kod opcode. Zamiast tego musi wrócić na szczyt pętli ewaluacyjnej, co ma oczywiste konsekwencje 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 plik ("silnik" wirtualnej maszyny Pythona):

Niektóre opcody mają tendencję do łączenia się w pary, dzięki czemu możliwe jest przewiduj drugi kod kiedy pierwszy jest uruchomiony. Na przykład, GET_ITER jest często po FOR_ITER. Oraz FOR_ITER jest często następnie STORE_FAST lub UNPACK_SEQUENCE.

Weryfikacja prognozy kosztuje pojedynczy szybki test rejestru zmienna względem stałej. Jeśli parowanie było dobre, to własnej gałęzi wewnętrznej procesora ma duże prawdopodobieństwo sukces, co skutkuje niemal zerowym przejściem na następny kod. Pomyślna prognoza oszczędza podróż przez eval-loop w tym jego dwie nieprzewidywalne gałęzie, test HAS_ARG i przełącznik. W połączeniu z wewnętrzną gałęzią procesora, pomyślne PREDICT powoduje, że oba opcody działają tak, jakby były one pojedynczym nowym kodem opcode z połączonymi ciałami.

Możemy zobaczyć w kodzie źródłowym dla FOR_ITER opcode dokladnie gdzie PREDYKCJA dla STORE_FAST jest wykonana:

case FOR_ITER:                         // the FOR_ITER opcode case
    v = TOP();
    x = (*v->ob_type->tp_iternext)(v); // x is the next value from iterator
    if (x != NULL) {                     
        PUSH(x);                       // put x on top of the stack
        PREDICT(STORE_FAST);           // predict STORE_FAST will follow - success!
        PREDICT(UNPACK_SEQUENCE);      // this and everything below is skipped
        continue;
    }
    // error-checking and more code for when the iterator ends normally                                     

Funkcja PREDICT rozszerza się do if (*next_instr == op) goto PRED_##op tzn. po prostu przeskakujemy na początek przewidywanego kodu opcode. W tym przypadku przeskakujemy tutaj:

PREDICTED_WITH_ARG(STORE_FAST);
case STORE_FAST:
    v = POP();                     // pop x back off the stack
    SETLOCAL(oparg, v);            // set it as the new local variable
    goto fast_next_opcode;

Zmienna lokalna jest teraz ustawiona i następny kod jest gotowy do wykonania. Python kontynuuje przez iterable aż do końca, dokonując pomyślnego przewidywania za każdym razem.

Strona wiki Pythona zawiera więcej informacji o tym, jak działa wirtualna maszyna Cpythona.

 29
Author: Alex Riley,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-03-28 09:16:31