web-dev-qa-db-ja.com

インタプリタによって維持される整数キャッシュとは何ですか?

Pythonのソースコードに飛び込んだ後、int(-5)からint(256)までの_PyInt_Object_ sの配列を維持していることがわかりました(@ 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_ sが作成されます(実際にはast_for_atom()関数で実行されます)が、後でマージされます。

_PyAST_Compile_と_PyEval_EvalCode_でソースを理解するのは難しいので、助けを求めるためにここにいます。誰かがヒントを教えてくれれば感謝しますか?

77
felix021

Pythonは [-5, 256] の範囲の整数をキャッシュするため、その範囲の整数も同じであることが期待されます。

表示されるのは、Pythonコンパイラが同じテキストの一部である場合に同一のリテラルを最適化するコンパイラです。

Python Shellと入力すると、各行は完全に異なるステートメントであり、別の瞬間に解析されるため、次のようになります。

>>> 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バイトコードコンパイラは(静的型言語のように)大規模な最適化を実行することはできませんが、想像以上に機能します。これらのことの1つは、リテラルの使用を分析して回避することですそれらを複製します。

キャッシュを持たないフロートに対しても機能するため、これはキャッシュとは関係ありません。

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

2つのPyInt_Objectが作成された理由について、リテラル比較を回避するためにguessしたと思います。たとえば、数字257は複数のリテラルで表すことができます。

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

パーサーには2つの選択肢があります。

  • 整数を作成する前に、リテラルを一般的なベースに変換し、リテラルが同等かどうかを確認します。次に、単一の整数オブジェクトを作成します。
  • 整数オブジェクトを作成し、それらが等しいかどうかを確認します。はいの場合、単一の値のみを保持してすべてのリテラルに割り当てます。それ以外の場合は、割り当てる整数が既にあります。

おそらくPythonパーサーは2番目のアプローチを使用します。これにより、変換コードを書き直す必要がなくなり、拡張も容易になります(たとえば、フロートでも機能します)。


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);
    }

ここに見られるように、パーサーはnot指定された値の整数をすでに見つけたかどうかをチェックします。これにより、2つの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オブジェクトに格納し、これからすぐに等しい定数が同じスロットで、最終的なバイトコードに単一の定数をもたらします。

83
Bakuriu