After dive into Python's source code, I find out that it maintains an array of PyInt_Object
s ranging from int(-5) to int(256) (@src/Objects/intobject.c)
A little experiment proves it:
>>> a = 1
>>> b = 1
>>> a is b
True
>>> a = 257
>>> b = 257
>>> a is b
False
But if I run those code together in a py file (or join them with semi-colons), the result is different:
>>> a = 257; b = 257; a is b
True
I'm curious why they are still the same object, so I digg deeper into the syntax tree and compiler, I came up with a calling hierarchy listed below:
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()
Then I added some debug code in PyInt_FromLong
and before/after PyAST_FromNode
, and executed a test.py:
a = 257
b = 257
print "id(a) = %d, id(b) = %d" % (id(a), id(b))
the output looks like:
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
It means that during the cst
to ast
transform, two different PyInt_Object
s are created (actually it's performed in the ast_for_atom()
function), but they are later merged.
I find it hard to comprehend the source in PyAST_Compile
and PyEval_EvalCode
, so I'm here to ask for help, I'll be appreciative if some one gives a hint?
Python caches integers in the range
[-5, 256]
, so it is expected that integers in that range are also identical.What you see is the Python compiler optimizing identical literals when part of the same text.
When typing in the Python shell each line is a completely different statement, parsed in a different moment, thus:
But if you put the same code into a file:
This happens whenever the parser has a chance to analyze where the literals are used, for example when defining a function in the interactive interpreter:
Note how the compiled code contains a single constant for the
257
.In conclusion, the Python bytecode compiler is not able to perform massive optimizations (like static types languages), but it does more than you think. One of these things is to analyze usage of literals and avoid duplicating them.
Note that this does not have to do with the cache, because it works also for floats, which do not have a cache:
For more complex literals, like tuples, it "doesn't work":
But the literals inside the tuple are shared:
Regarding why you see that two
PyInt_Object
are created, I'd guess that this is done to avoid literal comparison. for example, the number257
can be expressed by multiple literals:The parser has two choices:
Probably the Python parser uses the second approach, which avoids rewriting the conversion code and also it's easier to extend (for example it works with floats as well).
Reading the
Python/ast.c
file, the function that parses all numbers isparsenumber
, which callsPyOS_strtoul
to obtain the integer value (for intgers) and eventually callsPyLong_FromString
:As you can see here the parser does not check whether it already found an integer with the given value and so this explains why you see that two int objects are created, and this also means that my guess was correct: the parser first creates the constants and only afterward optimizes the bytecode to use the same object for equal constants.
The code that does this check must be somewhere in
Python/compile.c
orPython/peephole.c
, since these are the files that transform the AST into bytecode.In particular, the
compiler_add_o
function seems the one that does it. There is this comment incompiler_lambda
:So it seems like
compiler_add_o
is used to insert constants for functions/lambdas etc. Thecompiler_add_o
function stores the constants into adict
object, and from this immediately follows that equal constants will fall in the same slot, resulting in a single constant in the final bytecode.