¿Qué pasa con el caché de enteros mantenido por el intérprete?

Resuelto felix021 asked hace 11 años • 1 respuestas

Después de sumergirme en el código fuente de Python, descubrí que mantiene una matriz de PyInt_Objectmensajes de correo electrónico que van desde int(-5)hasta int(256)(@src/Objects/intobject.c)

Un pequeño experimento lo demuestra:

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

Pero si ejecuto esos códigos juntos en un archivo py (o los uno con punto y coma), el resultado es diferente:

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

Tengo curiosidad por saber por qué siguen siendo el mismo objeto, así que profundicé en el árbol de sintaxis y el compilador y se me ocurrió una jerarquía de llamadas que se enumera a continuación:

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

Luego agregué algo de código de depuración en PyInt_FromLongy antes/después PyAST_FromNodey ejecuté un test.py:

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

la salida se ve así:

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

Significa que durante la csttransformación ast, se crean dos PyInt_Objects diferentes (en realidad se realiza en la ast_for_atom()función), pero luego se fusionan.

Me resulta difícil comprender la fuente en PyAST_Compiley PyEval_EvalCode, así que estoy aquí para pedir ayuda. Agradecería que alguien me dé una pista.

felix021 avatar Mar 02 '13 13:03 felix021
Aceptado

Python almacena en caché los números enteros en el rango [-5, 256], por lo que los números enteros en ese rango generalmente , aunque no siempre, son idénticos.

Lo que ve para 257 es el compilador de Python que optimiza literales idénticos cuando se compila en el mismo objeto de código.

Al escribir en el shell de Python, cada línea es una declaración completamente diferente, analizada y compilada por separado, por lo tanto:

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

Pero si pones el mismo código en un archivo:

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

Esto sucede siempre que el compilador tiene la oportunidad de analizar los literales juntos, por ejemplo al definir una función en el intérprete interactivo:

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

Observe cómo el código compilado contiene una única constante para el archivo 257.

En conclusión, el compilador de código de bytes de Python no puede realizar optimizaciones masivas (como lenguajes escritos estáticamente), pero hace más de lo que cree. Una de estas cosas es analizar el uso de literales y evitar duplicarlos.

Tenga en cuenta que esto no tiene que ver con el caché, porque también funciona para flotantes, que no tienen caché:

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

Para literales más complejos, como tuplas, "no funciona":

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

Pero los literales dentro de la tupla son compartidos:

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

(Tenga en cuenta que el plegado constante y el optimizador de mirilla pueden cambiar el comportamiento incluso entre versiones de corrección de errores, por lo que los ejemplos regresan Trueo Falseson básicamente arbitrarios y cambiarán en el futuro).


En cuanto a por qué ves que PyInt_Objectse crean dos, supongo que esto se hace para evitar una comparación literal. por ejemplo, el número 257se puede expresar mediante varios literales:

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

El analizador tiene dos opciones:

  • Convierta los literales a alguna base común antes de crear el número entero y vea si los literales son equivalentes. luego cree un único objeto entero.
  • Crea los objetos enteros y comprueba si son iguales. En caso afirmativo, mantenga solo un valor y asígnelo a todos los literales; de lo contrario, ya tiene los números enteros para asignar.

Probablemente el analizador de Python utilice el segundo enfoque, que evita reescribir el código de conversión y también es más fácil de extender (por ejemplo, también funciona con flotantes).


Al leer el Python/ast.carchivo, la función que analiza todos los números es parsenumber, que llama PyOS_strtoulpara obtener el valor entero (para números enteros) y finalmente llama PyLong_FromString:

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

Como puede ver aquí, el analizador no verifica si ya encontró un número entero con el valor dado y esto explica por qué ve que se crean dos objetos int, y esto también significa que mi suposición era correcta: el analizador primero crea las constantes. y solo después optimiza el código de bytes para usar el mismo objeto para constantes iguales.

El código que hace esta verificación debe estar en algún lugar de Python/compile.co Python/peephole.c, ya que estos son los archivos que transforman el AST en código de bytes.

En particular la compiler_add_ofunción parece la que lo hace. Hay este comentario en 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;

Entonces parece que compiler_add_ose usa para insertar constantes para funciones/lambdas, etc. La compiler_add_ofunción almacena las constantes en un dictobjeto, y de esto se deduce inmediatamente que constantes iguales caerán en la misma ranura, lo que resultará en una única constante en el código de bytes final.

Bakuriu avatar Mar 02 '2013 07:03 Bakuriu