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


82

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때문에 도움을 요청하기 위해 여기에 있습니다. 누군가가 힌트를 주면 감사하겠습니다.


2
파이썬 소스가 어떻게 작동하는지 이해하려고 노력하고 있습니까, 아니면 파이썬으로 작성된 코드의 결과를 이해하려고합니까? Python으로 작성된 코드의 결과는 "이것은 구현 세부 사항이므로 발생 여부에 의존하지 마십시오"입니다.
BrenBarn 2013 년

구현 세부 사항에 의존하지 않을 것입니다. 그냥 궁금해서 소스 코드에 침입하려고합니다.
felix021 2013 년


@Blckknght 감사합니다. 나는 그 질문에 대한 답을 알고 있었고 그보다 더 나아갔습니다.
felix021 2013 년

답변:


103

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


감사. 정수기가이 작업을 수행하는 이유를 알고 있으며 int 및 float와 똑같이 작동하는 문자열을 이전에 테스트했으며 두 개의 Const (257)를 표시하는 compiler.parse ()를 사용하여 구문 트리를 인쇄했습니다. 나는 소스 코드에서 언제, 어떻게하는지 궁금합니다 ... 위에서 한 테스트는 정수기가 이미 a와 b에 대해 두 개의 PyInt_Object를 생성했음을 보여줍니다. 따라서 실제로 병합하는 의미는 거의 없습니다 (메모리 저장과는 별개).
felix021 2013 년

@ felix021 내 대답을 다시 업데이트했습니다. 두 개의 int가 생성 된 위치를 찾았고이를 처리하는 정확한 코드 줄을 아직 찾지 못했지만 최적화가 발생하는 파일을 알고 있습니다.
Bakuriu

매우 감사합니다! 나는 조심스럽게 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의 키로 시도하고 해당 튜플의 해시를 계산하는 동안 실제 정수만 값이 사용되므로 하나의 PyInt_Object 만 u_consts dict에 삽입됩니다.
felix021 2013 년

나는 win7에서 py2와 py3로 둘 다 False실행 a = 5.0; b = 5.0; print (a is b)한다
zhangxaochen

1
@zhangxaochen 대화 형 인터프리터에서 같은 줄에 또는 다른 줄에 두 문장을 작성 했습니까? 어쨌든 다른 버전의 파이썬은 다른 동작을 생성 할 수 있습니다. 내 컴퓨터에 그것은 수행 의 결과를 True(지금 다시 검사). 최적화는 구현 세부 사항이기 때문에 신뢰할 수 없으므로 대답에서 말하고 싶었던 요점을 무효화하지 않습니다. 또한 상수 compile('a=5.0;b=5.0', '<stdin>', 'exec')).co_consts만 있음을 보여줍니다 5.0(Linux의 python3.3에 있음).
Bakuriu
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.