Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python 개선 제안 한국어 번역

PEP 280 – 전역 변수 접근 최적화

Author:
Guido van Rossum <guido at python.org>
Status:
Deferred
Type:
Standards Track
Created:
10-Feb-2002
Python-Version:
2.3
Post-History:


Table of Contents

번역·라이선스 안내

이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판

보류

이 PEP는 좋은 아이디어이지만, 이 PEP와 PEP 266, PEP 267 사이의 차이점을 정리하는 작업을 맡아 나선 사람이 아직 없습니다. 따라서 이 PEP는 보류됩니다.

초록

이 PEP는 모듈 전역 변수 접근을 최적화하는 또 다른 접근 방식을 설명하며, PEP 266(Skip Montanaro의 전역 변수/속성 접근 최적화)과 PEP 267(Jeremy Hylton의 모듈 네임스페이스 접근 최적화)에 대한 대안을 제공합니다.

궁극적으로는 여러 접근 방식 중 하나가 선택되어 구현될 것으로 예상되며, 경우에 따라서는 여러 접근 방식이 먼저 시제품으로 제작될 수도 있습니다.

설명

(참고: Jason Orendorff는 다음과 같이 적었습니다: “””저는 이것을 오래 전, 아마 Python 1.5쯤 되는 버전에서 한 번 구현해 본 적이 있습니다. 일반 Python보다 15%만 느린 수준까지 만들었다가 그 뒤 포기했습니다. ;) 제 구현에서 “cell”은 진짜 일급(first-class) 객체였고, “celldict”는 딕셔너리를 복사해서 손댄 버전이었습니다. 나머지가 어떻게 동작했는지는 기억나지 않습니다.””” 참조: https://mail.python.org/pipermail/python-dev/2002-February/019876.html)

cell을, Python 객체를 가리키는 포인터 하나와 cell을 가리키는 포인터 하나를 담은 아주 단순한 Python 객체라고 합시다. 두 포인터는 모두 NULL일 수 있습니다. Python 구현은 다음과 같을 수 있습니다:

class cell(object):

    def __init__(self):
        self.objptr = NULL
        self.cellptr = NULL

cellptr 속성은 내장 객체(built-ins)를 검색하기 위해 cell들을 서로 연결하는 데 사용되며, 이는 뒤에서 설명됩니다.

celldict를, 문자열(모듈 전역 변수의 이름)에서 객체(그 전역 변수의 값)로의 매핑을, cell들의 딕셔너리를 이용해 구현한 것이라고 합시다. Python 구현은 다음과 같을 수 있습니다:

class celldict(object):

    def __init__(self):
        self.__dict = {} # dict of cells

    def getcell(self, key):
        c = self.__dict.get(key)
        if c is None:
            c = cell()
            self.__dict[key] = c
        return c

    def cellkeys(self):
        return self.__dict.keys()

    def __getitem__(self, key):
        c = self.__dict.get(key)
        if c is None:
            raise KeyError, key
        value = c.objptr
        if value is NULL:
            raise KeyError, key
        else:
            return value

    def __setitem__(self, key, value):
        c = self.__dict.get(key)
        if c is None:
            c = cell()
            self.__dict[key] = c
        c.objptr = value

    def __delitem__(self, key):
        c = self.__dict.get(key)
        if c is None or c.objptr is NULL:
            raise KeyError, key
        c.objptr = NULL

    def keys(self):
        return [k for k, c in self.__dict.iteritems()
                if c.objptr is not NULL]

    def items(self):
        return [k, c.objptr for k, c in self.__dict.iteritems()
                if c.objptr is not NULL]

    def values(self):
        preturn [c.objptr for c in self.__dict.itervalues()
                if c.objptr is not NULL]

    def clear(self):
        for c in self.__dict.values():
            c.objptr = NULL

    # Etc.

어떤 키에 해당하는 cell이 존재하지만 그 cell의 objptr이 NULL인 경우가 있을 수 있는데, 이러한 cell을 비어 있는(empty) cell이라고 부르겠습니다. celldict가 매핑으로 사용될 때는 마치 비어 있는 cell이 존재하지 않는 것처럼 취급됩니다. 하지만 일단 추가된 cell은 celldict에서 결코 삭제되지 않으며, getcell() 메서드를 사용하면 비어 있는 cell에도 접근할 수 있습니다.

celldict 구현은 cell의 cellptr 속성을 전혀 사용하지 않습니다.

모듈 구현을 변경하여 __dict__에 celldict를 사용하도록 합니다. 모듈의 getattr, setattr, delattr 연산은 이제 celldict의 getitem, setitem, delitem 연산에 대응됩니다. <module>.__dict__globals()의 타입이 아마 유일한 하위 호환성 문제일 것입니다.

모듈이 초기화될 때, 해당 모듈의 __builtins____builtin__ 모듈의 __dict__로부터 초기화되며, 이는 그 자체로 celldict입니다. __builtins__안의 각 cell에 대해, 새 모듈의 __dict__는 objptr이 NULL이고 cellptr이 __builtins__의 해당 cell을 가리키는 cell을 추가합니다. Python 의사 코드(rexec는 무시함):

import __builtin__

class module(object):

    def __init__(self):
        self.__dict__ = d = celldict()
        d['__builtins__'] = bd = __builtin__.__dict__
        for k in bd.cellkeys():
            c = self.__dict__.getcell(k)
            c.cellptr = bd.getcell(k)

    def __getattr__(self, k):
        try:
            return self.__dict__[k]
        except KeyError:
            raise IndexError, k

    def __setattr__(self, k, v):
        self.__dict__[k] = v

    def __delattr__(self, k):
        del self.__dict__[k]

컴파일러는 전역 변수 참조에 대해 LOAD_GLOBAL_CELL <i>(및 STORE_GLOBAL_CELL <i> 등) 옵코드를 생성하는데, 여기서 <i>LOAD_CONST의 const 인덱스처럼 하나의 코드 객체 안에서만 의미를 갖는 작은 인덱스입니다. 코드 객체는 co_globals라는 새로운 튜플을 가지며, 이는 <i>로 인덱싱되는 코드가 참조하는 전역 변수들의 이름을 담고 있습니다. 이를 수행하기 위해 새로운 분석은 전혀 필요하지 않습니다.

code object와 celldict로부터 함수 객체가 생성될 때, 함수 객체는 코드 객체의 co_globals에 있는 이름들에 대응하는 셀을 celldict에 요청하여 셀 포인터 배열을 생성합니다. celldict에 특정 이름에 대한 셀이 아직 없으면, 빈 셀을 생성합니다. 이 셀 포인터 배열은 함수 객체에 func_cells로 저장됩니다. celldict 대신 일반 딕셔너리로부터 함수 객체가 생성되면, func_cellsNULL 포인터입니다.

VM이 LOAD_GLOBAL_CELL <i> 명령어를 실행하면, func_cells에서 셀 번호 <i>를 가져옵니다. 그런 다음 셀의 PyObject 포인터를 확인하며, NULL이 아니면 그것이 전역 값입니다. NULL이면, 셀의 셀 포인터를 따라 다음 셀로 이동하며(NULL이 아닌 경우), 그 셀의 PyObject 포인터를 확인합니다. 그것마저도 NULL이거나 두 번째 셀이 없으면, NameError가 발생합니다. (NULL 셀 포인터를 찾을 때까지 셀 포인터 체인을 따라갈 수도 있지만, 저는 이것을 쓸 일이 없습니다.) STORE_GLOBAL_CELL <i>도 비슷하지만, 셀 포인터 체인을 따라가지 않고 항상 첫 번째 셀에 저장합니다.

함수의 전역이 celldict가 아니어서 func_cellsNULL인 경우를 위한 폴백이 VM에 있습니다. 이 경우, 코드 객체의 co_globals<i>로 인덱싱하여 해당 전역의 이름을 찾고, 이 이름을 사용해 함수의 전역 딕셔너리를 인덱싱합니다.

추가 아이디어

  • func_cell을 절대 NULL 포인터로 만들지 말고, 대신 빈 셀들의 배열을 만들어 LOAD_GLOBAL_CELLNULL 검사 없이 func_cells를 인덱싱할 수 있게 하십시오.
  • 셀이 생성될 때 c.cellptr을 c와 같도록 만들어, LOAD_GLOBAL_CELLNULL 검사 없이 항상 c.cellptr을 역참조할 수 있게 하십시오.

    이 두 가지 추가 아이디어를 더하면, LOAD_GLOBAL_CELL에 대한 Python 의사코드는 다음과 같습니다:

    def LOAD_GLOBAL_CELL(self, i):
        # self is the frame
        c = self.func_cells[i]
        obj = c.objptr
        if obj is not NULL:
            return obj # Existing global
        return c.cellptr.objptr # Built-in or NULL
    
  • 더 적극적으로 하십시오. 빌트인의 실제 값을 담은 셀에 대한 포인터가 아니라, 빌트인의 실제 값 자체를 모듈 딕셔너리에 넣으십시오.

    여기에는 두 가지 요점이 있습니다. (1) 가장 흔한 연산인 접근을 단순화하고 빠르게 합니다. (2) 극단적인 기존 코너 케이스의 충실한 에뮬레이션을 지원합니다.

    두 번째 요점과 관련하여, 위 방식에서 빌트인의 집합은 모듈 딕셔너리가 처음 생성되는 시점에 캡처됩니다. 그 이후 빌트인 이름 집합에 대한 변경은 모듈 딕셔너리에 반영되지 않습니다. 예시: main.pycheater.py 파일을 생각해 보십시오:

    [main.py]
    import cheater
    def f():
        cheater.cheat()
        return pachinko()
    print f()
    
    [cheater.py]
    def cheat():
        import __builtin__
        __builtin__.pachinko = lambda: 666
    

    main.py를 Python 2.2(또는 그 이전 버전)에서 실행하면 666이 출력됩니다. 그러나 이 제안 하에서는, main의 __dict__가 초기화되는 시점에 __builtin__.pachinko는 존재하지 않습니다. f에 대한 함수 객체가 생성될 때, main.__dict__에는 두 개의 NULL로 매핑되는 pachinko 셀이 추가됩니다. cheat()가 호출되면, __builtin__.__dict__에도 pachinko 셀이 추가되지만, main.__dict__는 그것을 알지 못하며 – 앞으로도 절대 알지 못할 것입니다. f의 return 문이 pachinko를 참조할 때, main.__dict__pachinko 셀에서 여전히 이중 NULL을 발견하게 되고, 그에 따라 NameError가 발생합니다.

    모듈 전역 foo가 del되었지만, 모듈 딕셔너리가 처음 생성된 이후이면서 그 이전에 빌트인 foo가 생성된 경우에도, (원인은 다르지만) 비슷한 호환성 붕괴가 발생할 수 있습니다. 그러면 2.2 및 그 이전 버전에서는 빌트인 foo가 모듈에서 보이지만, 이 제안 하에서는 계속 보이지 않게 됩니다.

    빌트인을 변경하는 일은 극히 드물기 때문에(대부분의 프로그램은 빌트인을 절대 변경하지 않으며, 빌트인을 자주 변경하는 그럴듯한 용도를 상상하기도 어렵습니다 – 저는 그런 경우를 본 적도 들은 적도 없습니다), 빌트인 변경 비용이 얼마나 커지든 상관없습니다. 반면, 전역과 빌트인을 참조하는 것은 매우 흔합니다. 이러한 관찰들을 종합하면, 모듈 전역에서 빌트인을 더 적극적으로 캐싱하여 접근을 빠르게 하는 대신, 캐시를 동기화 상태로 유지하기 위해 빌트인 변경 비용을 (상당히) 더 비싸게 만드는 방안이 제시됩니다.

    위 방식의 대부분은 그대로 유지되며, 나머지 대부분도 약간만 다를 뿐입니다. 셀은 다음과 같이 바뀝니다:

    class cell(object):
        def __init__(self, obj=NULL, builtin=0):
            self.objptr = obj
            self.builtinflag = builtin
    

    그리고 celldict는 문자열을 이 버전의 셀로 매핑합니다. builtinflag는 objptr이 내장 객체(builtins)로부터 얻은 값을 포함하고 있을 때만, 그리고 오직 그럴 때만 참입니다. 다시 말해, 셀이 캐시된 값으로서 동작하고 있을 때만, 그리고 오직 그럴 때만 참입니다. builtinflag가 거짓일 때, objptr은 모듈 전역 변수의 값입니다(NULL일 수도 있습니다). celldict는 다음과 같이 변경됩니다.:

    class celldict(object):
    
        def __init__(self, builtindict=()):
            self.basedict = builtindict
            self.__dict = d = {}
            for k, v in builtindict.items():
                d[k] = cell(v, 1)
    
        def __getitem__(self, key):
            c = self.__dict.get(key)
            if c is None or c.objptr is NULL or c.builtinflag:
                raise KeyError, key
            return c.objptr
    
        def __setitem__(self, key, value):
            c = self.__dict.get(key)
            if c is None:
                c = cell()
                self.__dict[key] = c
            c.objptr = value
            c.builtinflag = 0
    
        def __delitem__(self, key):
            c = self.__dict.get(key)
            if c is None or c.objptr is NULL or c.builtinflag:
                raise KeyError, key
            c.objptr = NULL
            # We may have unmasked a builtin.  Note that because
            # we're checking the builtin dict for that *now*, this
            # still works if the builtin first came into existence
            # after we were constructed.  Note too that del on
            # namespace dicts is rare, so the expense of this check
            # shouldn't matter.
            if key in self.basedict:
                c.objptr = self.basedict[key]
                assert c.objptr is not NULL # else "in" lied
                c.builtinflag = 1
            else:
                # There is no builtin with the same name.
                assert not c.builtinflag
    
        def keys(self):
            return [k for k, c in self.__dict.iteritems()
                    if c.objptr is not NULL and not c.builtinflag]
    
        def items(self):
            return [k, c.objptr for k, c in self.__dict.iteritems()
                    if c.objptr is not NULL and not c.builtinflag]
    
        def values(self):
            preturn [c.objptr for c in self.__dict.itervalues()
                    if c.objptr is not NULL and not c.builtinflag]
    
        def clear(self):
            for c in self.__dict.values():
                if not c.builtinflag:
                    c.objptr = NULL
    
        # Etc.
    

    속도상의 이점은 LOAD_GLOBAL_CELL을 단순화한 데서 비롯되는데, 이는 다른 모든 네임스페이스 연산을 합친 것보다 더 자주 실행될 것으로 예상됩니다.:

    def LOAD_GLOBAL_CELL(self, i):
        # self is the frame
        c = self.func_cells[i]
        return c.objptr   # may be NULL (also true before)
    

    즉, 내장 객체(builtins) 접근과 모듈 전역 변수 접근이 똑같이 빠릅니다. 모듈 전역 변수의 경우, NULL 포인터 검사+분기 하나가 절약됩니다. 내장 객체(builtins)의 경우, 추가적인 포인터 추적 하나도 절약됩니다.

    이를 실현하는 데 필요한 나머지 부분은 비용이 큰데, 바로 내장 객체(builtins)의 변경 사항을 내장 객체로부터 초기화된 모듈 딕셔너리들로 전파하는 것입니다. 이는 2.2 버전에서 새 스타일 베이스 클래스의 변경 사항을 그 자식들에게 전파하는 것과 매우 유사합니다. 즉, 내장 객체(builtins)는 자신의 딕셔너리로부터 초기화된 모듈들(또는 모듈 딕셔너리들)에 대한 약한 참조(weakref) 목록을 유지해야 합니다. 내장 객체(builtins) 딕셔너리에 변경(새 키 추가, 기존 키에 연결된 값 변경, 또는 키 삭제)이 가해지면, 모듈 딕셔너리 목록을 순회하며 그에 대응하는 변경을 가합니다. 이는 간단합니다. 예를 들어, 내장 객체(builtins)에서 키가 삭제되면 각 모듈에서 reflect_bltin_del을 실행합니다.:

    def reflect_bltin_del(self, key):
        c = self.__dict.get(key)
        assert c is not None # else we were already out of synch
        if c.builtinflag:
            # Put us back in synch.
            c.objptr = NULL
            c.builtinflag = 0
        # Else we're shadowing the builtin, so don't care that
        # the builtin went away.
    

    c.builtinflag는 동일한 이름의 모듈 전역 변수를 우리가 잘못 삭제하는 것을 막아준다는 점에 유의하십시오. 새로운 (키, 값) 내장 객체(builtin) 쌍을 추가하는 것도 비슷합니다.:

    def reflect_bltin_new(self, key, value):
        c = self.__dict.get(key)
        if c is None:
            # Never heard of it before:  cache the builtin value.
            self.__dict[key] = cell(value, 1)
        elif c.objptr is NULL:
            # This used to exist in the module or the builtins,
            # but doesn't anymore; rehabilitate it.
            assert not c.builtinflag
            c.objptr = value
            c.builtinflag = 1
        else:
            # We're shadowing it already.
            assert not c.builtinflag
    

    기존 내장 객체(builtin)의 값을 변경하는 경우:

    def reflect_bltin_change(self, key, newvalue):
        c = self.__dict.get(key)
        assert c is not None # else we were already out of synch
        if c.builtinflag:
            # Put us back in synch.
            c.objptr = newvalue
        # Else we're shadowing the builtin, so don't care that
        # the builtin changed.
    

자주 묻는 질문

  • 질문: 다음이 여전히 가능할까요?

    a) __builtin__ 네임스페이스에 새 내장 객체(builtins)를 설치하여 이미 로드된 모든 모듈에서 즉시 사용할 수 있도록 하는 것

    b) 내장 객체(builtins)(예: open())를 저만의 사본으로 오버라이드하되 (예를 들어 보안을 강화하기 위해) 이 새 사본이 모든 모듈에서 이전 것을 오버라이드하도록 만드는 것

    답변: 예, 이것이 바로 이 설계의 전체 요점입니다. 원래의 접근 방식에서는, LOAD_GLOBAL_CELL이 두 번째 셀에서 NULL을 발견하면 __builtins__ 딕셔너리가 수정되었는지 다시 확인해야 합니다(의사 코드에는 아직 이것이 반영되어 있지 않습니다). Tim의 “더 공격적인” 대안 역시 이를 처리합니다.

  • 질문: 새로운 방식은 제한된 실행 모델과 어떻게 어울리나요?

    답변: 이를 완전히 지원하도록 의도되었습니다.

  • 질문: 전역 변수가 삭제되면 어떤 일이 일어나나요?

    답변: 해당 모듈의 celldict는 그 키에 대해 objptr이 NULL인 셀을 가지게 됩니다. 이는 두 변형 모두에서 참이지만, “공격적인” 변형에서는 더 나아가 이것이 동일한 이름의 내장 객체(builtin)를 드러내는지 확인하고, 만약 그렇다면 그 값(최종적인 PyObject*의 단순한 포인터 복사)을 셀의 objptr에 복사하고 셀의 builtinflag를 참으로 설정합니다.

  • 질문: LOAD_GLOBAL_CELL의 C 코드는 어떤 모습일까요?

    답변: “추가 아이디어” 아래 첫 두 항목을 반영한 첫 번째 버전은 다음과 같은 모습일 수 있습니다.:

    case LOAD_GLOBAL_CELL:
        cell = func_cells[oparg];
        x = cell->objptr;
        if (x == NULL) {
            x = cell->cellptr->objptr;
            if (x == NULL) {
                ... error recovery ...
                break;
            }
        }
        Py_INCREF(x);
        PUSH(x);
        continue;
    

    이렇게 작성할 수도 있습니다(Ka-Ping Yee의 아이디어 제공에 감사드립니다).:

    case LOAD_GLOBAL_CELL:
        cell = func_cells[oparg];
        x = cell->cellptr->objptr;
        if (x != NULL) {
            Py_INCREF(x);
            PUSH(x);
            continue;
        }
        ... error recovery ...
        break;
    

    현대 CPU 아키텍처에서는, 이것이 내장 객체(builtins)에 대해 취해지는 분기의 수를 줄여주는데, 이는 정말 좋은 일일 수 있습니다. 반면 어지간히 괜찮은 메모리 캐시라면 일반 전역 변수의 경우 cell->cellptr이 cell과 동일하다는 것을 알아차릴 것이므로, 이 경우에도 매우 빠를 것입니다.

    공격적인 변형의 경우:

    case LOAD_GLOBAL_CELL:
        cell = func_cells[oparg];
        x = cell->objptr;
        if (x != NULL) {
            Py_INCREF(x);
            PUSH(x);
            continue;
        }
        ... error recovery ...
        break;
    
  • Q: func_cells 배열이 아마 없을 모듈의 최상위 코드에서는 어떤 일이 일어납니까?

    A: 코드 분석을 좀 수행하여 func_cells 배열을 만들 수도 있고, 전역 딕셔너리에 대해 PyMapping_GetItem을 사용해야 할 LOAD_NAME을 사용할 수도 있습니다.

그래픽

Ka-Ping Yee는 “import spam” 이후 상태를 그린 그림을 제공했는데, 여기서 spam.py는 다음을 포함합니다:

import eggs

i = -2
max = 3

def foo(n):
    y = abs(i) + max
    return eggs.ham(y + n)

그림은 http://web.lfw.org/repo/cells.gif 에 있으며, 더 큰 버전은 http://lfw.org/repo/cells-big.gif 에, 원본은 http://lfw.org/repo/cells.ai 에 있습니다.

비교

XXX 여기에 세 가지 접근 방식의 비교를 추가할 수 있습니다.