Что с целочисленным кэшем, поддерживаемым интерпретатором?

После погружения в исходный код 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_Object два разных PyInt_Object (фактически это выполнялось в функции 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 не может выполнять массивные оптимизации (например, языки статических типов), но он делает больше, чем вы думаете. Одна из этих вещей - анализировать использование литералов и избегать их дублирования.

Обратите внимание, что это не связано с кешем, поскольку оно работает также и для float, у которых нет кеша:

>>> 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 для получения целочисленного значения (для intgers) и в конечном итоге вызывает PyLong_FromString:

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

Как вы можете видеть здесь, синтаксический анализатор не проверяет, уже ли оно найдено целое число с заданным значением, и поэтому это объясняет, почему вы видите, что создаются два объекта int, а это также означает, что моя догадка верна: синтаксический анализатор сначала создает константы и только после этого оптимизирует байт-код для использования одного и того же объекта для равных констант.

Код, который выполняет эту проверку, должен находиться где-то в Python/compile.c или Python/peephole.c, так как это файлы, которые преобразуют AST в байт-код.

В частности, функция compiler_add_o кажется той, которая ее выполняет. Это комментарий в 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;

Таким образом, похоже, что compiler_add_o используется для вставки констант для функций /lambdas и т.д. Функция compiler_add_o сохраняет константы в объект dict, и из этого сразу следует, что равные константы будут попадать в один и тот же слот, в результате чего одна константа в финале байткод.