What's with the integer cache maintained by the interpreter?
Asked Answered
H

1

115

After dive into Python's source code, I find out that it maintains an array of PyInt_Objects 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_Objects 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?

Hawsepiece answered 2/3, 2013 at 6:50 Comment(4)
Are you just trying to understand how the Python source works, or are you trying to understand what the upshot is for code written in Python? Because the upshot for code written in Python is "this is an implementation detail, don't ever rely on it happening or not happening".Woodsum
I'm not going to rely on the implemention detail. I'm just curious and try to break into the source code.Hawsepiece
Related: Python "is" operator behaves unexpectedly with integersTallbot
@Tallbot thanks. I've known the answer of that question, and I go further than that.Hawsepiece
D
136

Python caches integers in the range [-5, 256], so integers in that range are usually but not always identical.

What you see for 257 is the Python compiler optimizing identical literals when compiled in the same code object.

When typing in the Python shell each line is a completely different statement, parsed and compiled separately, thus:

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

But if you put the same code into a file:

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

This happens whenever the compiler has a chance to analyze the literals together, for example when defining a function in the interactive interpreter:

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

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 statically typed 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:

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

For more complex literals, like tuples, it "doesn't work":

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

But the literals inside the tuple are shared:

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

(Note that constant folding and the peephole optimizer can change behaviour even between bugfix versions, so which examples return True or False is basically arbitrary and will change in the future).


Regarding why you see that two PyInt_Object are created, I'd guess that this is done to avoid literal comparison. for example, the number 257 can be expressed by multiple literals:

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

The parser has two choices:

  • Convert the literals to some common base before creating the integer, and see if the literals are equivalent. then create a single integer object.
  • Create the integer objects and see if they are equal. If yes, keep only a single value and assign it to all the literals, otherwise, you already have the integers to assign.

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 is parsenumber, which calls PyOS_strtoul to obtain the integer value (for intgers) and eventually calls PyLong_FromString:

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

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 or Python/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 in 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;

So it seems like compiler_add_o is used to insert constants for functions/lambdas etc. The compiler_add_o function stores the constants into a dict object, and from this immediately follows that equal constants will fall in the same slot, resulting in a single constant in the final bytecode.

Drysalt answered 2/3, 2013 at 7:54 Comment(13)
Thanks. I know why the intepreter do this, and I've also tested strings before, which acts just the same as int and float, and I've also printed the syntax tree using compiler.parse() which shows two Const(257). I'm just wondering when and how in the source code ... What's more the test I did above shows that the intepreter already created two PyInt_Object for a and b, so there's actually little meaning merging them (apart from save memory).Hawsepiece
@Hawsepiece I've updated my answer again. I found where the two ints are created and I know in which files the optimization happens, even though I still didn't find the exact line of code that handle that.Drysalt
Thanks very much! I carefully went over compile.c, the calling chain is 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). in the compoler_add_o(), python will try to if-not-find-then-set PyTuple(PyIntObject n, PyInt_Type) as key into c->u->u_consts, and while calculating the hash of that tuple, only the actual int value is used, so only one PyInt_Object will be inserted to the u_consts dict.Hawsepiece
I get False executing a = 5.0; b = 5.0; print (a is b) both with py2 and py3 on win7Aroid
@Aroid Did you write the two statements on the same line or on different lines in the interactive interpreter? Anyway, different versions of python can produce different behaviour. On my machine it does results in True (just rechecked now). Optimizations aren't reliable since they are just an implementation detail, so that doesn't invalidate the point I wanted to make in my answer. Also compile('a=5.0;b=5.0', '<stdin>', 'exec')).co_consts shows that there is only a 5.0 constant (in python3.3 on linux).Drysalt
@Drysalt on the same line. I'll take that as a proof of "unreliable" then. ;)Aroid
Try x=-6 y=-6 print(id(x), id(y)), their ids are equalCrescentia
@EmilyFitz No: >>> x = -6 >>> y = -6 >>> print(id(x), id(y)) 140043868274544 140043868721104 note the difference with using -5 which does yield the same id: >>> x = -5 >>> y = -5 >>> print(id(x), id(y)) 9751968 9751968 Moreover check what happens when using a=-4 and trying subtracting 1 or 2: >>> a = -4 >>> x = a-2 >>> y = a-2 >>> id(x), id(y) (140043868274544, 140043868721040) >>> x = a-1 >>> y = a-1 >>> id(x),id(y) (9751968, 9751968) As you can see up to -5 the id is always the same because it is cached, -6 is not and id changes everytime.Drysalt
@EmilyFitz That is not to say that if you write in a file multiple -6 constants the python compiler may optimize them to use a single object. But try to operate on that element and the id changes. If you do the same with operations resulting in -5 or an other value in the cache the id is always going to be the same.Drysalt
Doing a = (1,2); b = (1,2) and then checking for a is b actually produces True...Priapitis
@Priapitis Refer to this: #55945426 Constant folding is an optimization. It changes with versions. Typically newer versions tend to "constant fold" more, but it might not always be the case.Drysalt
@Drysalt thanks that's interesting. I use your answer a lot for duplicates on that subjects :) I recently stumbled on a question specifically about tuples and saw this remark. Wonderful answer!Priapitis
Side-note: As of 3.11, the pow case for small ints not being reduced to small int cache singletons (linked above as "but not always") will be fixed. They've been closing all the little holes where the small int cache wasn't properly used over time, but an extension module not using the limited API (directly accessing the internals of the PyLongObject structure) could always bypass said cache checks, so relying on it, even on CPython, will never be a safe thing.Tenebrous

© 2022 - 2024 — McMap. All rights reserved.