1. ホーム
  2. python

[解決済み] インタプリタが保持する整数キャッシュはどうなっているのか?

2023-01-26 17:48:24

質問

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 を変換すると、2つの異なる PyInt_Object が作成されます (実際には、この処理は ast_for_atom() 関数で実行されます) が、それらは後でマージされます。

のソースを理解するのは難しいですね。 PyAST_CompilePyEval_EvalCode ということで、助けを求めに来ました。もし誰かがヒントをくれるなら、感謝します。

どのように解決するのですか?

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)

コンパイルされたコードに含まれる定数が1つであることに注意してください。 257 .

結論として、Pythonのバイトコードコンパイラは(静的型付け言語のような)大規模な最適化を行うことはできませんが、あなたが思っている以上のことを行っています。そのひとつが、リテラルの使い方を分析し、重複を避けることです。

これはキャッシュを持たないfloatでも動作するため、キャッシュとは関係ないことに注意してください。

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

タプルのようなより複雑なリテラルでは、"doesn't work"です。

>>> 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 は基本的に任意であり、将来的に変更される可能性があります)。


その2つを見る理由について PyInt_Object が作成されることについてですが、私は と推測します。 は、リテラルな比較を避けるために行われるのだと思います。 257 は複数のリテラルで表現することができます。

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

パーサーは2つの選択肢を持っています。

  • 整数を作成する前にリテラルを共通のベースに変換し、リテラルが等価であるかどうかを確認します。
  • 整数オブジェクトを作成し、それらが等価であるかどうかを確認します。そうでなければ、すでに割り当てるべき整数があります。

おそらくPythonパーサーは2番目のアプローチを使用します。これは変換コードの書き換えを避けることができ、また拡張も簡単です(たとえば、floatでも動作します)。


を読むと 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 オブジェクトが 2 つ作成されていることがわかります。 つまり、パーサーは最初に定数を作成し、その後でバイトコードを最適化して同じ定数に同じオブジェクトを使用するのです。

このチェックを行うコードは 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 オブジェクトに格納します。このことから、同じ定数は同じスロットに入ることになり、最終的なバイトコードには1つの定数が含まれることになります。