101 votes

Qu'en est-il du cache des 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 allant de int(-5) a 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 si je les joins avec des points-virgules), le résultat est différent :

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

Je suis curieux de savoir pourquoi il s'agit toujours du même objet, alors je creuse plus profondément dans l'arbre syntaxique et le compilateur, je suis arrivé à une hiérarchie d'appel listé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()

Puis j'ai ajouté du code de débogage dans PyInt_FromLong et avant/après PyAST_FromNode et 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 le cst a ast transformer, deux différents PyInt_Object sont créées (en fait, cette opération est effectuée dans l'espace de travail de l'utilisateur). ast_for_atom() ), mais ils sont ensuite fusionnés.

Je trouve difficile de comprendre la source en PyAST_Compile y PyEval_EvalCode Je suis donc ici pour demander de l'aide. Je serais reconnaissant si quelqu'un me donnait un indice ?

124voto

Bakuriu Points 22607

Python met en cache les entiers dans l'intervalle [-5, 256] donc les nombres entiers dans cet intervalle sont généralement mais pas toujours identique.

Ce que vous voyez pour 257 est le compilateur Python qui optimise des littéraux identiques lorsqu'ils sont compilés dans le même objet de code.

Lorsque vous tapez dans le shell Python, chaque ligne est une déclaration complètement différente, analysée et compilée séparément, ainsi :

>>> 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 le compilateur a la possibilité d'analyser les littéraux ensemble, 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 l'élément 257 .

En conclusion, le compilateur de bytecode Python n'est pas capable d'effectuer des optimisations massives (comme les langages typés statiquement), 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 de les dupliquer.

Notez que cela n'a rien à 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

(Notez que le pliage constant et l'optimiseur peephole peuvent changer de comportement même entre les versions de correction de bogues, donc les exemples qui retournent True ou False est fondamentalement arbitraire et changera à l'avenir).


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

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

L'analyseur syntaxique a deux choix :

  • Convertissez les littéraux en une base commune avant de créer le nombre entier, et voyez si les littéraux sont équivalents. Créez ensuite un objet entier unique.
  • Créez les objets integer et voyez s'ils sont égaux. Si oui, gardez une seule valeur et assignez-la à tous les littéraux, sinon, vous avez déjà les entiers à assigner.

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


Lire le Python/ast.c la fonction qui analyse tous les nombres est parsenumber qui appelle PyOS_strtoul pour obtenir la valeur entière (pour les intgers) et appelle éventuellement 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, le parseur fait no vérifie s'il a déjà trouvé un entier avec la valeur donnée et cela explique donc pourquoi vous voyez que deux objets int sont créés, et cela signifie également que ma supposition était correcte : l'analyseur syntaxique crée d'abord les constantes et seulement après optimise le bytecode pour utiliser le même objet pour des constantes égales.

Le code qui effectue cette vérification doit se trouver quelque part dans le fichier Python/compile.c ou Python/peephole.c car ce sont les fichiers qui transforment l'AST en bytecode.

En particulier, le 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;

Donc il semble que compiler_add_o est utilisé pour insérer des constantes pour les fonctions/lambdas etc. Le site compiler_add_o stocke les constantes dans un fichier dict et il en découle immédiatement que des constantes égales tomberont dans le même slot, ce qui donne une constante unique dans le bytecode final.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X