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


85

Pythonのソースコードを調べたところPyInt_Objectint(-5)からint(256)(@ src / Objects / intobject.c)までの範囲のsの配列が維持されていることがわかりました。

少しの実験でそれが証明されます:

>>> 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_CompilePyEval_EvalCode、私は助けを求めるためにここにいるので、いくつかのいずれかのヒントを与えた場合、私は感謝だろうか、?


2
Pythonソースがどのように機能するかを理解しようとしているだけですか、それともPythonで記述されたコードの結果が何であるかを理解しようとしていますか?Pythonで記述されたコードの結果は「これは実装の詳細であり、発生しているかどうかに依存しないでください」です。
BrenBarn 2013年

実装の詳細に依存するつもりはありません。私はただ興味があり、ソースコードに侵入しようとしています。
felix021 2013年


@Blckknghtありがとう。私はその質問の答えを知っています、そして私はそれ以上に進みます。
felix021 2013年

回答:


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バイトコードコンパイラは(静的に型付けされた言語のように)大規模な最適化を実行することはできませんが、思った以上のことを実行します。これらの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

(定数畳み込みとのぞき穴オプティマイザーはバグ修正バージョン間でも動作を変更する可能性があるため、どの例が返されるTrueFalse、基本的に任意であり、将来変更されることに注意してください)。


あなたはその二つが見理由についてPyInt_Object作成され、私はしたいと思い、これは文字通りの比較を避けるために行われていること。たとえば、数値257は複数のリテラルで表すことができます。

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

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

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

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


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

ここでわかるように、パーサーは指定された値の整数を既に検出したかどうかをチェックしないため、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オブジェクトに格納します。これにより、等しい定数が同じスロットに配置され、最終的なバイトコードに単一の定数が作成されます。


ありがとう。インタープリターがこれを行う理由を知っています。また、intやfloatとまったく同じように機能する文字列もテストしました。また、2つのConst(257)を示すcompiler.parse()を使用して構文ツリーを出力しました。ソースコードでいつ、どのように行うのか疑問に思っています...さらに、上記のテストでは、インタープリターがaとbに対して2つのPyInt_Objectを既に作成していることが示されているため、実際にはそれらをマージする意味はほとんどありません(メモリの保存を除く)。
felix021 2013年

@ felix021回答を再度更新しました。2つのintが作成された場所を見つけ、それを処理するコードの正確な行がまだ見つからなかったとしても、最適化がどのファイルで行われるかを知っています。
バクリウ2013

どうもありがとう!compile.cを注意深く調べました。呼び出しチェーンは、compiler_visit_stmt-> VISIT(c、expr、e)-> compiler_visit_expr(c、e)-> ADDOP_O(c、LOAD_CONST、e-> v.Num.n、consts)です。 ->コンパイラ_addop_o(c、LOAD_CONSTS、c-> u-> u_consts、e-> v.Num.n)->コンパイラ_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のみ値が使用されるため、1つのPyInt_Objectのみがu_constsdictに挿入されます。
felix021 2013年

私はwin7でpy2とpy3の両方をFalse実行a = 5.0; b = 5.0; print (a is b)します
zhangxaochen 2014

1
@zhangxaochenインタラクティブインタプリタの同じ行または異なる行に2つのステートメントを記述しましたか?とにかく、Pythonのバージョンが異なれば、動作も異なります。私のマシンでは、結果出ますTrue(今すぐ再チェックしました)。最適化は単なる実装の詳細であるため信頼できません。そのため、私の答えで言いたかった点が無効になることはありません。またcompile('a=5.0;b=5.0', '<stdin>', 'exec')).co_consts5.0定数のみがあることも示しています(Linuxのpython3.3)。
バクリウ2014
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.