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

85

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

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

felix021
источник
2
Вы просто пытаетесь понять, как работает исходный код Python, или пытаетесь понять, каков результат для кода, написанного на Python? Потому что результатом кода, написанного на Python, является «это деталь реализации, никогда не полагайтесь на то, что это произойдет или не произойдет».
BrenBarn 02
Я не буду полагаться на детали реализации. Мне просто любопытно, и я пытаюсь взломать исходный код.
felix021 02
@Blckknght, спасибо. Я знаю ответ на этот вопрос и иду дальше.
felix021 02

Ответы:

106

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

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


Что касается того, почему вы видите, что 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);
    }

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

Бакуриу
источник
Благодарю. Я знаю, почему интерпретатор делает это, и раньше я также тестировал строки, которые действуют точно так же, как int и float, и я также распечатал дерево синтаксиса с помощью compiler.parse (), которое показывает две Const (257). Мне просто интересно, когда и как в исходном коде ... Более того, тест, который я сделал выше, показывает, что интерпретатор уже создал два PyInt_Object для a и b, поэтому на самом деле нет особого смысла в их объединении (кроме сохранения памяти).
felix021 02
@ felix021 Я снова обновил свой ответ. Я обнаружил, где создаются два целых числа, и знаю, в каких файлах происходит оптимизация, хотя до сих пор не нашел точной строки кода, которая это обрабатывает.
Бакуриу
Спасибо большое! Я внимательно просмотрел compile.c, цепочка вызовов: 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). в compoler_add_o () python будет пытаться если-не-найти-затем установить PyTuple (PyIntObject n, PyInt_Type) в качестве ключа в c-> u-> u_consts, и при вычислении хэша этого кортежа будет использоваться только фактический int используется значение, поэтому в dict u_consts будет вставлен только один PyInt_Object.
felix021 05
Я Falseвыполняю a = 5.0; b = 5.0; print (a is b)как py2, так и py3 на win7
zhangxaochen
1
@zhangxaochen Вы написали два оператора в одной строке или в разных строках в интерактивном интерпретаторе? В любом случае разные версии python могут вести себя по-разному. На моей машине это делает результаты True(только перепроверены сейчас). Оптимизация ненадежна, поскольку это всего лишь деталь реализации, так что это не отменяет того момента, который я хотел высказать в своем ответе. Также compile('a=5.0;b=5.0', '<stdin>', 'exec')).co_constsпоказывает, что есть только 5.0константа (в python3.3 на linux).
Bakuriu