Вопрос-Ответ

What's with the integer cache maintained by the interpreter?

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

После погружения в исходный код Python я обнаруживаю, что он поддерживает массив PyInt_Objects в диапазоне от 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 файла (фактически это выполняется в ast_for_atom() функции), но позже они объединяются.

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

Переведено автоматически
Ответ 1

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

То, что вы видите для 257, - это компилятор 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

(Обратите внимание, что постоянное сворачивание и оптимизатор peephole могут изменять поведение даже между версиями исправлений, поэтому то, какие примеры возвращают True или False, в основном произвольно и изменится в будущем).


Что касается того, почему вы видите, что созданы два 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 or 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 используется для вставки констант для функций / лямбд и т.д.
compiler_add_o Функция сохраняет константы в dict объекте, и из этого сразу следует, что равные константы попадут в один и тот же слот, в результате чего в конечном байт-коде будет одна константа.

python