O co chodzi z pamięcią podręczną liczb całkowitych obsługiwaną przez interpreter?

85

Po zanurzeniu się w kodzie źródłowym Pythona stwierdzam, że utrzymuje on tablicę PyInt_Objects odint(-5) do int(256)(@ src / Objects / intobject.c)

Mały eksperyment to potwierdza:

>>> a = 1
>>> b = 1
>>> a is b
True
>>> a = 257
>>> b = 257
>>> a is b
False

Ale jeśli uruchomię ten kod razem w pliku py (lub połączę je średnikami), wynik będzie inny:

>>> a = 257; b = 257; a is b
True

Jestem ciekawy, dlaczego wciąż są tym samym obiektem, więc zagłębiłem się w drzewo składni i kompilator, znalazłem hierarchię wywołań wymienioną poniżej:

PyRun_FileExFlags() 
    mod = PyParser_ASTFromFile() 
        node *n = PyParser_ParseFileFlagsEx() //source to cst
            parsetoke() 
                ps = PyParser_New() 
                for (;;)
                    PyTokenizer_Get() 
                    PyParser_AddToken(ps, ...)
        mod = PyAST_FromNode(n, ...)  //cst to ast
    run_mod(mod, ...)
        co = PyAST_Compile(mod, ...) //ast to CFG
            PyFuture_FromAST()
            PySymtable_Build()
            co = compiler_mod()
        PyEval_EvalCode(co, ...)
            PyEval_EvalCodeEx()

Następnie dodałem kod debugowania w PyInt_FromLongi przed / po PyAST_FromNodeoraz wykonałem test.py:

a = 257
b = 257
print "id(a) = %d, id(b) = %d" % (id(a), id(b))

wyjście wygląda następująco:

DEBUG: before PyAST_FromNode
name = a
ival = 257, id = 176046536
name = b
ival = 257, id = 176046752
name = a
name = b
DEBUG: after PyAST_FromNode
run_mod
PyAST_Compile ok
id(a) = 176046536, id(b) = 176046536
Eval ok

Oznacza to, że w okresie cstdoast przekształcić dwa różne PyInt_Objects tworzone są (w rzeczywistości jest to wykonywane w ast_for_atom()funkcji), ale są one później połączyły.

Trudno mi zrozumieć źródło w PyAST_Compilei PyEval_EvalCode, więc jestem tutaj, aby poprosić o pomoc, będę wdzięczny, jeśli ktoś daje wskazówkę?

felix021
źródło
2
Czy po prostu próbujesz zrozumieć, jak działa źródło Pythona, czy próbujesz zrozumieć, jaki jest skutek dla kodu napisanego w Pythonie? Ponieważ rezultatem kodu napisanego w Pythonie jest „to jest szczegół implementacji, nigdy nie polegaj na tym, że wydarzy się lub nie wydarzy się”.
BrenBarn
Nie będę polegać na szczegółach implementacji. Jestem po prostu ciekawy i próbuję włamać się do kodu źródłowego.
felix021
@Blckknght thanks. Znam odpowiedź na to pytanie i idę dalej.
felix021

Odpowiedzi:

106

Python buforuje liczby całkowite w zakresie [-5, 256], więc oczekuje się, że liczby całkowite w tym zakresie są również identyczne.

To, co widzisz, to kompilator Pythona optymalizujący identyczne literały, gdy są częścią tego samego tekstu.

Podczas pisania w powłoce Pythona każda linia jest zupełnie inną instrukcją, analizowaną w innym momencie, a więc:

>>> a = 257
>>> b = 257
>>> a is b
False

Ale jeśli umieścisz ten sam kod w pliku:

$ echo 'a = 257
> b = 257
> print a is b' > testing.py
$ python testing.py
True

Dzieje się tak za każdym razem, gdy parser ma szansę przeanalizować, gdzie są używane literały, na przykład podczas definiowania funkcji w interaktywnym interpretatorze:

>>> def test():
...     a = 257
...     b = 257
...     print a is b
... 
>>> dis.dis(test)
  2           0 LOAD_CONST               1 (257)
              3 STORE_FAST               0 (a)

  3           6 LOAD_CONST               1 (257)
              9 STORE_FAST               1 (b)

  4          12 LOAD_FAST                0 (a)
             15 LOAD_FAST                1 (b)
             18 COMPARE_OP               8 (is)
             21 PRINT_ITEM          
             22 PRINT_NEWLINE       
             23 LOAD_CONST               0 (None)
             26 RETURN_VALUE        
>>> test()
True
>>> test.func_code.co_consts
(None, 257)

Zwróć uwagę, jak skompilowany kod zawiera jedną stałą dla 257.

Podsumowując, kompilator kodu bajtowego Pythona nie jest w stanie wykonywać masowych optymalizacji (takich jak języki statyczne), ale robi więcej niż myślisz. Jedną z tych rzeczy jest analiza użycia literałów i unikanie ich dublowania.

Zauważ, że nie ma to nic wspólnego z pamięcią podręczną, ponieważ działa również dla pływaków, które nie mają pamięci podręcznej:

>>> a = 5.0
>>> b = 5.0
>>> a is b
False
>>> a = 5.0; b = 5.0
>>> a is b
True

W przypadku bardziej złożonych literałów, takich jak krotki, „nie działa”:

>>> a = (1,2)
>>> b = (1,2)
>>> a is b
False
>>> a = (1,2); b = (1,2)
>>> a is b
False

Ale dosłowne wewnątrz krotki są udostępniane:

>>> a = (257, 258)
>>> b = (257, 258)
>>> a[0] is b[0]
False
>>> a[1] is b[1]
False
>>> a = (257, 258); b = (257, 258)
>>> a[0] is b[0]
True
>>> a[1] is b[1]
True

(Zauważ, że ciągłe zwijanie i optymalizator wizjera mogą zmieniać zachowanie nawet między wersjami poprawek, więc które przykłady powracają Truelub Falsesą w zasadzie arbitralne i zmieniają się w przyszłości).


Jeśli chodzi o to, dlaczego widzisz, że dwa PyInt_Objectsą tworzone, przypuszczam, że ma to na celu uniknięcie dosłownego porównania. na przykład liczbę 257można wyrazić wieloma literałami:

>>> 257
257
>>> 0x101
257
>>> 0b100000001
257
>>> 0o401
257

Parser ma dwie możliwości:

  • Przekonwertuj literały na jakąś wspólną podstawę przed utworzeniem liczby całkowitej i sprawdź, czy literały są równoważne. następnie utwórz pojedynczy obiekt typu integer.
  • Utwórz obiekty typu integer i zobacz, czy są równe. Jeśli tak, zachowaj tylko jedną wartość i przypisz ją do wszystkich literałów, w przeciwnym razie masz już liczby całkowite do przypisania.

Prawdopodobnie parser Pythona używa drugiego podejścia, które pozwala uniknąć przepisywania kodu konwersji, a także jest łatwiejsze do rozszerzenia (na przykład działa również z liczbami zmiennoprzecinkowymi).


Czytając Python/ast.cplik, funkcja analizująca wszystkie liczby to parsenumber, która wywołuje PyOS_strtouluzyskanie wartości całkowitej (dla liczb całkowitych) i ostatecznie wywołuje PyLong_FromString:

    x = (long) PyOS_strtoul((char *)s, (char **)&end, 0);
    if (x < 0 && errno == 0) {
        return PyLong_FromString((char *)s,
                                 (char **)0,
                                 0);
    }

Jak widać, parser tego nie robi sprawdza, czy już znalazł liczbę całkowitą o podanej wartości, więc to wyjaśnia, dlaczego widzisz, że zostały utworzone dwa obiekty int, a to również oznacza, że ​​moje przypuszczenie było poprawne: parser najpierw tworzy stałe i dopiero później optymalizuje kod bajtowy, aby używać tego samego obiektu dla równych stałych.

Kod, który wykonuje to sprawdzenie, musi znajdować się gdzieś w Python/compile.clubPython/peephole.c , ponieważ są to pliki, które przekształcają AST na kod bajtowy.

W szczególności compiler_add_ofunkcja wydaje się być tą, która to robi. Jest taki komentarz w compiler_lambda:

/* Make None the first constant, so the lambda can't have a
   docstring. */
if (compiler_add_o(c, c->u->u_consts, Py_None) < 0)
    return 0;

Wygląda więc na to, że compiler_add_ojest używany do wstawiania stałych dla funkcji / lambd itp. compiler_add_oFunkcja przechowuje stałe do dictobiektu, a stąd natychmiast wynika, że ​​równe stałe będą znajdować się w tym samym gnieździe, dając pojedynczą stałą w końcowym kodzie bajtowym.

Bakuriu
źródło
Dzięki. Wiem, dlaczego intepreter to robi, a wcześniej testowałem również łańcuchy, które działają tak samo jak int i float, a także wydrukowałem drzewo składni za pomocą compiler.parse (), które pokazuje dwie Const (257). Zastanawiam się tylko, kiedy i jak w kodzie źródłowym ... Co więcej, test, który przeprowadziłem powyżej, pokazuje, że intepreter utworzył już dwa PyInt_Object dla a i b, więc właściwie nie ma sensu ich scalać (poza oszczędzaniem pamięci).
felix021
@ felix021 Ponownie zaktualizowałem odpowiedź. Znalazłem miejsce, w którym powstają dwie inte i wiem, w których plikach zachodzi optymalizacja, mimo że nadal nie znalazłem dokładnej linii kodu, która to obsługuje.
Bakuriu
Dziękuję bardzo! Uważnie przeszedłem przez compile.c, łańcuch wywołujący to compiler_visit_stmt -> VISIT (c, expr, e) -> compiler_visit_expr (c, e) -> ADDOP_O (c, LOAD_CONST, e-> v.Num.n, consts) -> compiler_addop_o (c, LOAD_CONSTS, c-> u-> u_consts, e-> v.Num.n) -> compiler_add_o (c, c-> u-> u_consts, e-> v.Num.n). w compoler_add_o (), python spróbuje ustawić PyTuple (PyIntObject n, PyInt_Type) if-not-find-then-set jako klucz do c-> u-> u_consts, a podczas obliczania skrótu tej krotki, tylko rzeczywisty int value jest używana, więc tylko jeden PyInt_Object zostanie wstawiony do dict u_consts.
felix021
I get Falsewykonywania a = 5.0; b = 5.0; print (a is b)zarówno z Py2 i py3 na win7
zhangxaochen
1
@zhangxaochen Czy napisałeś te dwa zdania w tej samej linii, czy w różnych wierszach w interaktywnym tłumaczu? W każdym razie różne wersje Pythona mogą powodować różne zachowania. Na moim komputerze daje to wyniki True(właśnie sprawdzone ponownie). Optymalizacje nie są niezawodne, ponieważ są tylko szczegółem implementacji, więc nie unieważnia tego, co chciałem zawrzeć w mojej odpowiedzi. compile('a=5.0;b=5.0', '<stdin>', 'exec')).co_constsPokazuje również , że istnieje tylko 5.0stała (w pythonie3.3 w systemie Linux).
Bakuriu,