Что с Integer Cache внутри Python?

После погружения в исходный код Python я обнаружил, что он поддерживает массив PyInt_Objectв диапазоне от int(-5) до int(256) (@src/Objects/intobject.c)

Небольшой эксперимент доказывает это:

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

Но если я запускаю этот код вместе в py-файле (или соединяю их точкой с запятой), результат будет другим:

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

Мне любопытно, почему они все еще являются одним и тем же объектом, поэтому я копаю глубже в синтаксическое дерево и компилятор, я придумал иерархию вызовов, перечисленную ниже:

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

Затем я добавил отладочный код в PyInt_FromLong и до / после PyAST_FromNodeи выполнил test.py:

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

вывод выглядит так:

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

Это означает, что во время cst в ast преобразовать, два разных PyInt_Objects созданы (на самом деле это выполняется в ast_for_atom() функции), но они позже объединяются.

Мне трудно понять источник в PyAST_Compile а также PyEval_EvalCodeтак что я здесь, чтобы попросить о помощи, я буду признателен, если кто-то намекнет?

1 ответ

Решение

Python кэширует целые числа в диапазоне [-5, 256], поэтому ожидается, что целые числа в этом диапазоне также идентичны.

То, что вы видите, это компилятор Python, оптимизирующий идентичные литералы, когда они являются частью одного и того же текста.

При наборе в оболочке Python каждая строка представляет собой совершенно другое утверждение, анализируемое в другой момент, таким образом:

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

Но если вы поместите тот же код в файл:

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

Это происходит всякий раз, когда парсер имеет возможность проанализировать, где используются литералы, например, при определении функции в интерактивном интерпретаторе:

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

Обратите внимание, что скомпилированный код содержит одну константу для 257,

В заключение, компилятор байт-кода Python не способен выполнять масштабную оптимизацию (например, языки статических типов), но он делает больше, чем вы думаете. Одна из них - проанализировать использование литералов и избежать их дублирования.

Обратите внимание, что это не имеет отношения к кешу, потому что он работает и для плавающих объектов, которые не имеют кеша:

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

Для более сложных литералов, таких как кортежи, это "не работает":

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

Но литералы внутри кортежа являются общими:

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

Относительно того, почему вы видите, что два PyInt_Object созданы, я думаю, это сделано, чтобы избежать буквального сравнения. например, число 257 может быть выражено несколькими литералами:

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

У парсера есть два варианта:

  • Преобразуйте литералы в некоторую общую базу перед созданием целого числа и посмотрите, эквивалентны ли литералы. затем создайте один целочисленный объект.
  • Создайте целочисленные объекты и посмотрите, равны ли они. Если да, оставьте только одно значение и присвойте его всем литералам, в противном случае у вас уже есть целые числа для назначения.

Вероятно, синтаксический анализатор Python использует второй подход, который позволяет избежать переписывания кода преобразования, а также его легче расширять (например, он работает и с плавающей точкой).


Чтение Python/ast.c файл, функция, которая анализирует все числа parsenumber, который вызывает PyOS_strtoul получить целочисленное значение (для целых чисел) и в конечном итоге вызывает 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 или же Python/peephole.c, since these are the files that transform the AST into bytecode.

В частности, 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.

Другие вопросы по тегам