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:
번역·라이선스 안내
이 비공식 한국어 번역은 원문 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)”은 진짜 일급 객체였고, “celldict”는 딕셔너리를 복사해서 뜯어고친 버전이었습니다. 나머지가 어떻게 동작했는지는 잊어버렸습니다.””” 참고 자료: https://mail.python.org/pipermail/python-dev/2002-February/019876.html)
셀을 Python 객체에 대한 포인터 하나와 셀에 대한 포인터 하나를 담는 아주 단순한 Python 객체라고 합시다. 두 포인터 모두 NULL일 수 있습니다. Python 구현은 다음과 같을 수 있습니다:
class cell(object):
def __init__(self):
self.objptr = NULL
self.cellptr = NULL
cellptr 속성은 내장을 검색하기 위해 셀들을 서로 연결하는 데 사용됩니다. 이에 대해서는 뒤에서 설명하겠습니다.
celldict를 문자열(모듈 전역 변수의 이름)에서 객체(그 전역 변수의 값)로의 매핑으로서, 셀의 dict를 사용해 구현한 것이라고 합시다. 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.
주어진 키에 대응하는 셀이 존재하지만 그 셀의 objptr이 NULL인 경우가 있을 수 있습니다. 이러한 셀을 빈 셀이라고 부릅시다. celldict를 매핑으로 사용할 때는 빈 셀이 존재하지 않는 것처럼 동작합니다. 그러나 한 번 추가된 셀은 celldict에서 절대 삭제되지 않으며, getcell() 메서드를 사용하면 빈 셀에 접근할 수 있습니다.
celldict 구현은 셀의 cellptr 속성을 전혀 사용하지 않습니다.
모듈 구현을 변경하여 모듈의 __dict__로 celldict를 사용하도록 합니다. 이제 모듈의 getattr, setattr, delattr 연산은 celldict의 getitem, setitem, delitem에 대응합니다. <module>.__dict__와 globals()의 타입이 아마도 유일한 하위 호환성 문제일 것입니다.
모듈이 초기화될 때, 모듈의 __builtins__는 __builtin__ 모듈의 __dict__로부터 초기화되며, 이 __dict__ 역시 celldict입니다. __builtins__의 각 셀에 대해, 새 모듈의 __dict__는 objptr이 NULL이고 cellptr이 __builtins__의 대응하는 셀을 가리키는 셀을 추가합니다. 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의 상수 인덱스처럼 하나의 코드 객체 안에서만 의미를 갖는 작은 인덱스입니다. 코드 객체에는 새로운 튜플인 co_globals가 추가되며, 이 튜플은 코드가 참조하는 전역 변수의 이름을 <i>로 인덱싱하여 제공합니다. 이를 위해 새로운 분석이 필요하지는 않습니다.
코드 객체와 celldict로부터 함수 객체가 생성될 때, 함수 객체는 코드 객체의 co_globals에 있는 이름에 대응하는 셀을 celldict에 요청하여 셀 포인터 배열을 만듭니다. celldict에 특정 이름에 대한 셀이 아직 없으면, celldict는 빈 셀을 새로 만듭니다. 이 셀 포인터 배열은 함수 객체에 func_cells로 저장됩니다. 함수 객체가 celldict가 아닌 일반 딕셔너리로부터 생성되면 func_cells는 NULL 포인터입니다.
VM이 LOAD_GLOBAL_CELL <i> 명령어를 실행하면, func_cells에서 셀 번호 <i>를 가져옵니다. 그런 다음 셀의 PyObject 포인터를 확인하고, NULL이 아니면 그것이 전역 값입니다. NULL이면 셀의 셀 포인터가 NULL이 아닌 경우 그 포인터를 따라 다음 셀로 가서, 그 셀의 PyObject 포인터를 확인합니다. 그것 역시 NULL이거나 두 번째 셀이 없으면 NameError가 발생합니다. (NULL 셀 포인터를 만날 때까지 셀 포인터 체인을 계속 따라갈 수도 있지만, 저는 그럴 필요가 없습니다.) STORE_GLOBAL_CELL <i>도 비슷하지만, 셀 포인터 체인을 따라가지 않고 항상 첫 번째 셀에 저장한다는 점이 다릅니다.
함수의 전역 이름공간이 celldict가 아니어서 func_cells가 NULL인 경우를 위해 VM에는 대체 경로가 있습니다. 그 경우에는 코드 객체의 co_globals를 <i>로 인덱싱하여 대응하는 전역 변수의 이름을 찾고, 이 이름으로 함수의 전역 딕셔너리를 인덱싱합니다.
추가 아이디어
func_cell을 절대NULL포인터로 만들지 말고, 대신 빈 셀의 배열을 만들어 두어LOAD_GLOBAL_CELL이NULL검사 없이func_cells를 인덱싱할 수 있게 하십시오.- 셀이 생성될 때
c.cellptr를 c와 같게 설정하여,LOAD_GLOBAL_CELL이NULL검사 없이 항상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) 기존의 극단적인 코너 케이스를 충실하게 에뮬레이션할 수 있도록 지원합니다.
2번과 관련하여, 위 방식에서 내장 객체의 집합은 모듈 딕셔너리가 처음 생성되는 시점에 포착됩니다. 그 이후에 내장 이름 집합에 가해진 변경은 모듈 딕셔너리에 반영되지 않습니다. 예:
main.py와cheater.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__에는 두 개의NULLs에 매핑되는 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에 내장에서 얻은 값이 들어 있을 때, 그리고 오직 그때에만 참입니다. 다시 말해, 셀이 캐시된 값으로 동작할 때, 그리고 오직 그때에만 참입니다.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)
즉, 내장에 접근하는 것과 모듈 전역 변수에 접근하는 것이 똑같이 빠릅니다. 모듈 전역 변수의 경우, NULL 포인터 테스트와 브랜치 하나가 절약됩니다. 내장의 경우, 추가적인 포인터 추적도 하나 절약됩니다.
이 방식이 제대로 동작하게 하는 데 필요한 나머지 부분은 비용이 많이 드는데, 바로 내장에 대한 변경을 내장으로부터 초기화된 모듈 딕셔너리들에 전파하는 것입니다. 이는 2.2에서 새 스타일 베이스 클래스의 변경 사항을 그 자손들에게 전파하는 것과 매우 비슷합니다. 내장은 내장 딕셔너리로부터 초기화된 모듈(또는 모듈 딕셔너리)에 대한 약한 참조 목록을 유지해야 합니다. 내장 딕셔너리에 변경(새 키 추가, 기존 키에 연결된 값 변경, 또는 키 삭제)이 발생하면, 모듈 딕셔너리 목록을 순회하면서 각각에 대응하는 변경을 적용하십시오. 이는 간단합니다. 예를 들어, 내장에서 키가 삭제되면 각 모듈에서
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덕분에 같은 이름의 모듈 전역 변수를 실수로 삭제하는 일이 없다는 점에 유의하십시오. 새 (키, 값) 내장 쌍을 추가하는 것도 비슷합니다: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
기존 내장의 값 변경:
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.
자주 묻는 질문
- Q: 다음 작업이 여전히 가능합니까?
a)
__builtin__네임스페이스에 새 내장 함수를 설치하고, 이미 로드된 모든 모듈에서 이를 즉시 사용할 수 있게 하려면 어떻게 합니까?b) 내장 함수(예:
open())를 저만의 복사본으로 재정의하되(예: 보안을 강화하기 위해), 이 새 복사본이 모든 모듈에서 이전 것을 재정의하도록 하려면 어떻게 합니까?A: 네, 그것이 바로 이 설계의 핵심입니다. 원래 방식에서는
LOAD_GLOBAL_CELL이 두 번째 셀에서NULL을 발견하면__builtins__딕셔너리가 수정되었는지 다시 확인해야 합니다(의사 코드에는 아직 이 부분이 없습니다). Tim의 “더 공격적인” 대안도 이 문제를 처리합니다. - Q: 새 방식은 제한 실행 모델과 어떻게 어울립니까?
A: 제한 실행 모델을 완전히 지원하도록 의도되었습니다.
- Q: 전역 변수가 삭제되면 어떻게 됩니까?
A: 모듈의 celldict에는 해당 키에 대해 objptr이
NULL인 셀이 남게 됩니다. 이는 두 변형 모두에서 마찬가지이지만, “공격적인” 변형은 여기서 더 나아가 이로 인해 같은 이름의 내장이 드러나는지 확인하고, 그렇다면 그 값(최종PyObject*의 포인터 복사일 뿐입니다)을 셀의 objptr에 복사하고 셀의builtinflag를 참으로 설정합니다. - Q:
LOAD_GLOBAL_CELL의 C 코드는 어떤 모습입니까?A: “추가 아이디어” 아래의 처음 두 항목을 반영한 첫 번째 버전은 다음과 같을 수 있습니다:
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 아키텍처에서는 이 방식이 내장에 대해 실행되는 브랜치의 수를 줄여 주는데, 이는 정말 좋은 일일 수 있습니다. 한편 쓸 만한 메모리 캐시라면 일반 전역 변수의 경우
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 여기에 세 가지 접근 방식의 비교를 추가할 수 있습니다.
Copyright
This document has been placed in the public domain.