Was ist mit dem vom Interpreter verwalteten Integer-Cache?


82

Nachdem ich in Pythons Quellcode eingetaucht bin, stelle ich fest, dass es ein Array von PyInt_Objects von int(-5)bis int(256)(@ src / Objects / intobject.c) enthält.

Ein kleines Experiment beweist es:

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

Wenn ich diesen Code jedoch zusammen in einer py-Datei ausführe (oder sie mit Semikolons verbinde), ist das Ergebnis anders:

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

Ich bin gespannt, warum sie immer noch dasselbe Objekt sind. Deshalb habe ich mich eingehender mit dem Syntaxbaum und dem Compiler befasst und eine Aufrufhierarchie gefunden, die unten aufgeführt ist:

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()

Dann habe ich Debug-Code in PyInt_FromLongund vor / nach hinzugefügt PyAST_FromNodeund eine test.py ausgeführt:

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

Die Ausgabe sieht aus wie:

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

Es bedeutet , dass während der cstzu asttransformieren, zwei verschiedene PyInt_Objects erstellt werden (eigentlich ist es in der ausgeführt ist ast_for_atom()Funktion), aber sie später zusammengeführt werden.

Es fällt mir schwer, die Quelle zu verstehen, PyAST_Compileund PyEval_EvalCodedeshalb bin ich hier, um um Hilfe zu bitten. Ich bin dankbar, wenn jemand einen Hinweis gibt.


2
Versuchen Sie nur zu verstehen, wie die Python-Quelle funktioniert, oder versuchen Sie zu verstehen, was das Ergebnis für in Python geschriebenen Code ist? Da das Ergebnis für in Python geschriebenen Code lautet: "Dies ist ein Implementierungsdetail. Verlassen Sie sich niemals darauf, dass es passiert oder nicht passiert."
BrenBarn

Ich werde mich nicht auf die Implementierungsdetails verlassen. Ich bin nur neugierig und versuche, in den Quellcode einzudringen.
felix021


@Blckknght danke. Ich habe die Antwort auf diese Frage gekannt und gehe noch weiter.
felix021

Antworten:


103

Python speichert Ganzzahlen im Bereich zwischen [-5, 256], sodass erwartet wird, dass auch Ganzzahlen in diesem Bereich identisch sind.

Was Sie sehen, ist der Python-Compiler, der identische Literale optimiert, wenn sie Teil desselben Textes sind.

Beim Eingeben der Python-Shell ist jede Zeile eine völlig andere Anweisung, die in einem anderen Moment analysiert wird.

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

Wenn Sie jedoch denselben Code in eine Datei einfügen:

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

Dies geschieht immer dann, wenn der Parser die Möglichkeit hat, zu analysieren, wo die Literale verwendet werden, beispielsweise beim Definieren einer Funktion im interaktiven Interpreter:

>>> 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)

Beachten Sie, wie der kompilierte Code eine einzelne Konstante für die enthält 257.

Zusammenfassend lässt sich sagen, dass der Python-Bytecode-Compiler keine massiven Optimierungen durchführen kann (wie Sprachen mit statischen Typen), aber mehr als Sie denken. Eines dieser Dinge ist, die Verwendung von Literalen zu analysieren und zu vermeiden, sie zu duplizieren.

Beachten Sie, dass dies nicht mit dem Cache zu tun hat, da dies auch für Floats funktioniert, die keinen Cache haben:

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

Für komplexere Literale wie Tupel "funktioniert es nicht":

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

Aber die Literale im Tupel werden geteilt:

>>> 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

In Bezug darauf, warum Sie sehen, dass zwei PyInt_Objecterstellt werden, würde ich vermuten, dass dies getan wird, um einen wörtlichen Vergleich zu vermeiden. Zum Beispiel kann die Zahl 257durch mehrere Literale ausgedrückt werden:

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

Der Parser hat zwei Möglichkeiten:

  • Konvertieren Sie die Literale in eine gemeinsame Basis, bevor Sie die Ganzzahl erstellen, und prüfen Sie, ob die Literale gleichwertig sind. Erstellen Sie dann ein einzelnes ganzzahliges Objekt.
  • Erstellen Sie die ganzzahligen Objekte und prüfen Sie, ob sie gleich sind. Wenn ja, behalten Sie nur einen einzigen Wert bei und weisen Sie ihn allen Literalen zu. Andernfalls müssen Sie bereits die Ganzzahlen zuweisen.

Wahrscheinlich verwendet der Python-Parser den zweiten Ansatz, der das Umschreiben des Konvertierungscodes vermeidet und auch einfacher zu erweitern ist (zum Beispiel funktioniert er auch mit Floats).


Beim Lesen der Python/ast.cDatei werden alle Zahlen analysiert. Diese Funktion parsenumberruft PyOS_strtoulauf, um den ganzzahligen Wert (für Ganzzahlen) zu erhalten, und ruft schließlich Folgendes auf PyLong_FromString:

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

Wie Sie hier sehen können, prüft der Parser nicht , ob er bereits eine Ganzzahl mit dem angegebenen Wert gefunden hat. Dies erklärt, warum Sie sehen, dass zwei int-Objekte erstellt werden, und dies bedeutet auch, dass meine Vermutung richtig war: Der Parser erstellt zuerst die Konstanten und erst danach wird der Bytecode optimiert, um dasselbe Objekt für gleiche Konstanten zu verwenden.

Der Code, der diese Prüfung durchführt, muss sich irgendwo in Python/compile.coder befinden Python/peephole.c, da dies die Dateien sind, die den AST in Bytecode umwandeln.

Insbesondere compiler_add_oscheint die Funktion diejenige zu sein, die dies tut. Es gibt diesen Kommentar in 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;

Es scheint also, als würde compiler_add_oes verwendet, um Konstanten für Funktionen / Lambdas usw. einzufügen. Die compiler_add_oFunktion speichert die Konstanten in einem dictObjekt, und daraus folgt sofort, dass gleiche Konstanten in denselben Slot fallen, was zu einer einzelnen Konstante im endgültigen Bytecode führt.


Vielen Dank. Ich weiß, warum der Interpreter dies tut, und ich habe auch zuvor Zeichenfolgen getestet, die genauso wie int und float funktionieren, und ich habe den Syntaxbaum auch mit compiler.parse () gedruckt, das zwei Const (257) zeigt. Ich frage mich nur, wann und wie im Quellcode ... Der Test, den ich oben durchgeführt habe, zeigt außerdem, dass der Interpreter bereits zwei PyInt_Object für a und b erstellt hat, sodass es eigentlich wenig Sinn macht, sie zusammenzuführen (abgesehen vom Speichern von Speicher).
felix021

@ felix021 Ich habe meine Antwort erneut aktualisiert. Ich habe herausgefunden, wo die beiden Ints erstellt wurden, und ich weiß, in welchen Dateien die Optimierung stattfindet, obwohl ich immer noch nicht die genaue Codezeile gefunden habe, die das handhabt.
Bakuriu

Vielen Dank! Ich habe compile.c sorgfältig durchgesehen. Die aufrufende Kette lautet 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). In compoler_add_o () versucht Python, PyTuple (PyIntObject n, PyInt_Type) als Schlüssel für c-> u-> u_consts festzulegen, während der Hash dieses Tupels berechnet wird, nur das tatsächliche int Der Wert wird verwendet, sodass nur ein PyInt_Object in das Diktat u_consts eingefügt wird.
felix021

Ich werde die FalseAusführung a = 5.0; b = 5.0; print (a is b)sowohl mit py2 und py3 auf win7
zhangxaochen

1
@zhangxaochen Haben Sie die beiden Anweisungen im interaktiven Interpreter in derselben Zeile oder in unterschiedlichen Zeilen geschrieben? Auf jeden Fall können verschiedene Versionen von Python ein unterschiedliches Verhalten hervorrufen. Auf meinem Computer führt dies zu True(gerade überprüft). Optimierungen sind nicht zuverlässig, da sie nur ein Implementierungsdetail sind, sodass der Punkt, den ich in meiner Antwort ansprechen wollte, nicht ungültig wird. Auch compile('a=5.0;b=5.0', '<stdin>', 'exec')).co_constszeigt , dass es nur eine ist 5.0konstant (in python3.3 unter Linux).
Bakuriu
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.