web-dev-qa-db-fra.com

Qu'en est-il du cache d'entiers maintenu par l'interpréteur?

Après avoir plongé dans le code source de Python, je découvre qu'il maintient un tableau de PyInt_Object S allant de int(-5) à int(256) (@ src/Objects/intobject.c)

Une petite expérience le prouve:

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

Mais si j'exécute ces codes ensemble dans un fichier py (ou les joint avec des points-virgules), le résultat est différent:

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

Je suis curieux de savoir pourquoi ils sont toujours le même objet, donc je fouille plus profondément dans l'arbre de syntaxe et le compilateur, j'ai trouvé une hiérarchie d'appels répertoriée ci-dessous:

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

Ensuite, j'ai ajouté du code de débogage dans PyInt_FromLong Et avant/après PyAST_FromNode, Et j'ai exécuté un test.py:

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

la sortie ressemble à:

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

Cela signifie que pendant la transformation cst en ast, deux PyInt_Object Différents sont créés (en fait, ils sont exécutés dans la fonction ast_for_atom()), mais ils sont fusionné plus tard.

J'ai du mal à comprendre la source dans PyAST_Compile Et PyEval_EvalCode, Donc je suis ici pour demander de l'aide, je serai reconnaissant si quelqu'un donne un indice?

77
felix021

Python met en cache les entiers de la plage [-5, 256] , il est donc prévu que les entiers de cette plage soient également identiques.

Ce que vous voyez est le compilateur Python optimisant des littéraux identiques lorsqu'ils font partie du même texte.

Lorsque vous saisissez le Python Shell chaque ligne est une instruction complètement différente, analysée à un moment différent, donc:

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

Mais si vous mettez le même code dans un fichier:

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

Cela se produit chaque fois que l'analyseur a une chance d'analyser où les littéraux sont utilisés, par exemple lors de la définition d'une fonction dans l'interpréteur interactif:

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

Notez comment le code compilé contient une seule constante pour le 257.

En conclusion, le compilateur de bytecode Python n'est pas en mesure d'effectuer des optimisations massives (comme les langages de types statiques), mais il fait plus que vous ne le pensez. L'une de ces choses est d'analyser l'utilisation des littéraux et d'éviter les dupliquer.

Notez que cela n'a pas à voir avec le cache, car cela fonctionne également pour les flottants, qui n'ont pas de cache:

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

Pour les littéraux plus complexes, comme les tuples, cela "ne fonctionne pas":

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

Mais les littéraux à l'intérieur du Tuple sont partagés:

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

En ce qui concerne la raison pour laquelle vous voyez que deux PyInt_Object Sont créés, je devine que cela est fait pour éviter la comparaison littérale. par exemple, le nombre 257 peut être exprimé par plusieurs littéraux:

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

L'analyseur a deux choix:

  • Convertissez les littéraux en une base commune avant de créer l'entier et voyez si les littéraux sont équivalents. puis créez un seul objet entier.
  • Créez les objets entiers et voyez s'ils sont égaux. Si oui, ne conservez qu'une seule valeur et affectez-la à tous les littéraux, sinon, vous avez déjà les entiers à affecter.

Probablement le Python utilise la deuxième approche, ce qui évite de réécrire le code de conversion et il est également plus facile à étendre (par exemple, il fonctionne également avec les flottants).


En lisant le fichier Python/ast.c, La fonction qui analyse tous les nombres est parsenumber, qui appelle PyOS_strtoul Pour obtenir la valeur entière (pour les entiers) et appelle finalement PyLong_FromString :

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

Comme vous pouvez le voir ici, l'analyseur ne pas vérifie s'il a déjà trouvé un entier avec la valeur donnée et cela explique pourquoi vous voyez que deux objets int sont créés, et cela signifie également que ma supposition était correct: l'analyseur crée d'abord les constantes et optimise ensuite le bytecode pour utiliser le même objet pour des constantes égales.

Le code qui effectue cette vérification doit être quelque part dans Python/compile.c Ou Python/peephole.c, Car ce sont les fichiers qui transforment le AST en bytecode.

En particulier, la fonction compiler_add_o Semble être celle qui le fait. Il y a ce commentaire dans 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;

Il semble donc que compiler_add_o Soit utilisé pour insérer des constantes pour les fonctions/lambdas etc. La fonction compiler_add_o Stocke les constantes dans un objet dict, et de là découle immédiatement que des constantes égales tombera dans le même emplacement, résultant en une seule constante dans le bytecode final.

83
Bakuriu