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

82

Después de sumergirme en el código fuente de Python, descubrí que mantiene una matriz de PyInt_Objects 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 ese código 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, 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é un código de depuración en PyInt_FromLongy antes / después PyAST_FromNode, y 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 transformación cstto ast, PyInt_Objectse crean dos s 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, por lo que estoy aquí para pedir ayuda, estaré agradecido si alguien da una pista

felix021
fuente
2
¿Está tratando de comprender cómo funciona la fuente de Python o está tratando de comprender cuál es el resultado del código escrito en Python? Debido a que el resultado del código escrito en Python es "este es un detalle de implementación, nunca confíe en que suceda o no suceda".
BrenBarn
No voy a confiar en los detalles de implementación. Solo tengo curiosidad y trato de entrar en el código fuente.
felix021
@Blckknght gracias. He conocido la respuesta a esa pregunta y voy más allá.
felix021

Respuestas:

103

Python almacena en caché los enteros en el rango [-5, 256], por lo que se espera que los enteros en ese rango también sean idénticos.

Lo que ves es el compilador de Python que optimiza literales idénticos cuando forman parte del mismo texto.

Al escribir en el shell de Python, cada línea es una declaración completamente diferente, analizada en un momento diferente, así:

>>> 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 analizador tiene la oportunidad de analizar dónde se utilizan los literales, 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)

Tenga en cuenta cómo el código compilado contiene una única constante para 257.

En conclusión, el compilador de código de bytes de Python no puede realizar optimizaciones masivas (como lenguajes de tipos estáticos), 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 la 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 se comparten:

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

Con respecto 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 257puede expresarse mediante varios literales:

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

El analizador tiene dos opciones:

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

Probablemente el analizador de Python usa 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 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, por lo que esto explica por qué ve que se crean dos objetos int, y esto también significa que mi conjetura fue 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 realiza 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 las constantes iguales caerán en la misma ranura, lo que resultará en una única constante en el bytecode final.

Bakuriu
fuente
Gracias. Sé por qué el intérprete hace esto, y también he probado cadenas antes, que actúan de la misma manera que int y float, y también imprimí el árbol de sintaxis usando compiler.parse () que muestra dos Const (257). Me pregunto cuándo y cómo en el código fuente ... Además, la prueba que hice arriba muestra que el intérprete ya creó dos PyInt_Object para ayb, por lo que en realidad tiene poco sentido fusionarlos (aparte de guardar memoria).
felix021
@ felix021 He actualizado mi respuesta nuevamente. Encontré dónde se crean las dos entradas y sé en qué archivos ocurre la optimización, aunque todavía no encontré la línea exacta de código que maneja eso.
Bakuriu
¡Muchas gracias! Revisé cuidadosamente compile.c, la cadena de llamada es 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). en el compoler_add_o (), python intentará if-not-find-then-set PyTuple (PyIntObject n, PyInt_Type) como clave en c-> u-> u_consts, y mientras calcula el hash de esa tupla, solo el int real se usa el valor, por lo que solo se insertará un PyInt_Object en el dict u_consts.
felix021
Consigo Falsela ejecución de a = 5.0; b = 5.0; print (a is b)ambos con AP2 y AP3 en Win7
zhangxaochen
1
@zhangxaochen ¿Escribió las dos declaraciones en la misma línea o en líneas diferentes en el intérprete interactivo? De todos modos, diferentes versiones de Python pueden producir un comportamiento diferente. En mi máquina, da como resultado True(solo se volvió a verificar ahora). Las optimizaciones no son confiables ya que son solo un detalle de implementación, por lo que eso no invalida el punto que quería hacer en mi respuesta. También compile('a=5.0;b=5.0', '<stdin>', 'exec')).co_constsmuestra que solo hay una 5.0constante (en python3.3 en linux).
Bakuriu