[python] 인터프리터가 유지 관리하는 정수 캐시는 무엇입니까?

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

그런 다음 일부 디버그 코드를 in PyInt_FromLong및 before / after 에 추가 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_Object들 (실제로는 수행 것 생성 ast_for_atom()기능),하지만 나중에 병합됩니다.

PyAST_Compile및 의 출처를 이해하기가 어렵 기 PyEval_EvalCode때문에 도움을 요청하기 위해 여기에 있습니다. 누군가가 힌트를 주면 감사하겠습니다.



답변

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.

결론적으로 파이썬 바이트 코드 컴파일러는 (정적 유형 언어와 같은) 대규모 최적화를 수행 할 수 없지만 생각보다 더 많은 작업을 수행합니다. 이러한 것 중 하나는 리터럴의 사용을 분석하고 중복을 피하는 것입니다.

캐시가없는 플로트에서도 작동하므로 캐시와 관련이 없습니다.

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

PyInt_Object개가 생성 되는 이유에 관해서 는 문자 적 ​​비교를 피하기 위해 이것이 수행되었다고 생각 합니다. 예를 들어 숫자 257는 여러 리터럴로 표현할 수 있습니다.

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

파서에는 두 가지 선택 사항이 있습니다.

  • 정수를 만들기 전에 리터럴을 몇 가지 공통 기준으로 변환하고 리터럴이 동일한 지 확인하십시오. 그런 다음 단일 정수 개체를 만듭니다.
  • 정수 객체를 만들고 동일한 지 확인합니다. 그렇다면 단일 값만 유지하고 모든 리터럴에 할당합니다. 그렇지 않으면 할당 할 정수가 이미 있습니다.

아마도 Python 파서는 두 번째 접근 방식을 사용하여 변환 코드를 다시 작성하지 않고 확장하기도 더 쉽습니다 (예를 들어 부동 소수점에서도 작동 함).


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

여기에서 볼 수 있듯이 파서는 주어진 값을 가진 정수를 이미 찾았는지 여부를 확인 하지 않으므로 두 개의 int 객체가 생성되는 이유를 설명하고 이것은 내 추측이 옳다는 것을 의미합니다. 파서는 먼저 상수를 생성합니다. 이후에만 동일한 상수에 대해 동일한 객체를 사용하도록 바이트 코드를 최적화합니다.

이 검사를 수행하는 코드 는 AST를 바이트 코드로 변환하는 파일이기 때문에 Python/compile.c또는에 있어야합니다 Python/peephole.c.

특히 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객체에 저장하고 , 이로부터 동일한 상수가 동일한 슬롯에 속하게되어 최종 바이트 코드에서 단일 상수가 생성됩니다.


답변