Що з цілочисельним кешем, що підтримується інтерпретатором?


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тому я тут, щоб попросити про допомогу, буду вдячний, якщо хтось підкаже?


2
Ви просто намагаєтеся зрозуміти, як працює джерело Python, або намагаєтесь зрозуміти, що таке результат для коду, написаного на Python? Оскільки результатом для коду, написаного на Python, є "це деталь реалізації, ніколи не покладайтесь на те, що це відбувається або не відбувається".
BrenBarn 02.03.13

Я не збираюся покладатися на деталі реалізації. Мені просто цікаво і намагаюся проникнути у вихідний код.
felix021 02.03.13


@Blckknght дякую. Я знав відповідь на це запитання і піду далі.
felix021 02.03.13

Відповіді:


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

@ felix021 Я знову оновив свою відповідь. Я знайшов, де створені два вставки, і я знаю, в яких файлах відбувається оптимізація, хоча я все ще не знайшов точного рядка коду, який це обробляє.
Бакуріу 03.03.13

Дуже дякую! Я обережно переглянув 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 спробує if-not-find-then-set PyTuple (PyIntObject n, PyInt_Type) як ключ у c-> u-> u_consts, і під час обчислення хешу цього кортежу, лише фактичний int використовується значення, тому до дикту u_consts буде вставлено лише один PyInt_Object.
felix021

Я отримую 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).
Бакуріу
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.