¿Qué pasa con la caché de enteros dentro de Python?

Después de sumergirme en el código fuente de Python, descubro que mantiene una matriz de PyInt_Object va desde int (-5) a 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 si me uno a ellos 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, por lo que profundizo en el árbol de syntax y el comstackdor, se me ocurrió una jerarquía de llamadas 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_FromLong y antes / después de 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 como:

 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 cst a ast , se crean dos PyInt_Object s diferentes (en realidad se realiza en la función ast_for_atom() ), pero luego se fusionan.

Me resulta difícil comprender la fuente en PyAST_Compile y PyEval_EvalCode , así que estoy aquí para pedir ayuda. ¿Estaré agradecido si alguien me da una pista?

Python almacena en caché 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 comstackdor de Python que optimiza literales idénticos cuando forma parte del mismo texto.

Al escribir en el shell de Python, cada línea es una statement completamente diferente, analizada en un momento diferente, 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 cuando 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) 

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

En conclusión, el comstackdor de bytecode 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 el caché, porque funciona también para flotadores, que no tienen un 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 las 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 

Con respecto a por qué ves que se crean dos PyInt_Object , supongo que esto se hace para evitar la comparación literal. por ejemplo, el número 257 se puede express mediante múltiples literales:

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

El analizador tiene dos opciones:

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

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


Leyendo el archivo Python/ast.c , la función que analiza todos los números es parsenumber , que llama a PyOS_strtoul para obtener el valor entero (para intgers) y finalmente llama a 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 comprueba si ya encontró un número entero con el valor dado, por lo que explica por qué ve que se crean dos objetos int, y esto también significa que mi suposición 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 comprobación debe estar en algún lugar de Python/compile.c o Python/compile.c , ya que estos son los archivos que transforman el AST en código de Python/compile.c .

En particular, la función compiler_add_o 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; 

Así que parece que compiler_add_o se usa para insertar constantes para funciones / lambdas, etc. La función compiler_add_o almacena las constantes en un objeto dict , y de esto sigue inmediatamente que constantes iguales caerán en la misma ranura, dando como resultado una constante única en la final bytecode