PEP 329 – 표준 라이브러리에서 내장 요소를 상수로 취급하기
- Author:
- Raymond Hettinger <python at rcn.com>
- Status:
- Rejected
- Type:
- Standards Track
- Created:
- 18-Apr-2004
- Python-Version:
- 2.4
- Post-History:
- 18-Apr-2004
번역·라이선스 안내
이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판
초록
내장 참조를 상수로 취급하는 함수를 추가하고, 표준 라이브러리 전체에 해당 함수를 적용하자는 제안입니다.
상태
이 PEP는 저자가 스스로 거부했습니다. ASPN 레시피는 좋은 반응을 얻었지만, 이를 핵심 배포판에 포함하는 방안을 고려하려는 의지는 더 적었습니다.
Jython 구현은 바이트코드를 사용하지 않으므로, 현재의 _len=len최적화를 제거하면 성능이 저하됩니다.
또한 바이트코드를 변경하는 것은 성능을 개선하고 더 깔끔한 코딩을 가능하게 하는 방법 중 가장 깔끔하지 못한 방법에 속합니다. 더 견고한 해결책은 최적화할 수 있는 대상을 나타내는 컴파일러 pragma 지시문이나 메타변수( const/volatile 선언과 유사함)를 사용하는 방식일 가능성이 높습니다.
동기
라이브러리에는 느린 전역 조회 대신 빠른 지역 참조를 생성하기 위한 _len=len 같은 코드가 포함되어 있습니다. 이러한 구성은 성능에 필요하지만 코드를 복잡하게 만들며, 대개 불완전합니다(많은 기회를 놓칩니다).
제안이 채택되면 코드베이스에서 이러한 구성을 제거하는 동시에 성능 측면에서 그 결과를 개선할 수 있습니다.
현재 라이브러리에는 while 1인스턴스가 100개 넘게 있습니다. 성능상의 이유로 가독성이 더 높은 while True로 대체되지 않았습니다(컴파일러는 True가 항상 상수라는 것을 알지 못하므로 검사를 제거할 수 없습니다). True를 상수로 변환하면 성능을 유지하면서 코드가 더 명확해집니다.
그 밖의 많은 기본 Python 연산도 전역 조회 때문에 훨씬 느리게 실행됩니다. try/except 문에서는 포착된 예외가 일치하는지 검사하기 전에 동적으로 조회됩니다. 마찬가지로 while x is not None 같은 단순한 동일성 검사는 매번 순회할 때마다 None 변수를 다시 조회해야 합니다. 내장 요소 조회가 특히 심각한 이유는 바깥쪽 전역 스코프를 먼저 검사해야 하기 때문입니다. 이러한 조회 체인은 다른 곳에서 더 유용하게 사용할 수 있는 캐시 공간을 소모합니다.
요컨대 제안이 채택되면 코드가 더 깔끔해지고 전반적인 성능이 향상됩니다.
제안
bind_constants()와 bind_all()이라는 두 함수를 포함하는 codetweaks.py라는 모듈을 추가하십시오. 첫 번째 함수는 상수 바인딩을 수행하고, 두 번째 함수는 대상 모듈의 모든 함수와 클래스에 이를 재귀적으로 적용합니다.
표준 라이브러리의 대부분의 모듈에서는 스크립트 끝부분에 두 줄을 추가하십시오.:
import codetweaks, sys
codetweaks.bind_all(sys.modules[__name__])
내장 요소를 바인딩하는 것 외에도 sre_compile 같은 일부 모듈에서는 모듈 변수를 내장 요소와 함께 상수로 바인딩하는 것도 타당합니다.
질문과 답변
- 이렇게 하면 모두가 최적화 문제로 관심을 돌리게 됩니까?
자동으로 수행되므로 최적화에 대해 고민해야 할 필요가 줄어듭니다.
- 간단히 말해, 어떻게 작동합니까?
모든 함수에는 바이트코드(파이썬 가상 머신의 언어)와 상수 테이블을 담은 속성이 있습니다. bind 함수는 바이트코드에서
LOAD_GLOBAL명령어를 검색하고 해당 값이 이미 알려져 있는지 확인합니다. 그렇다면 해당 값을 상수 테이블에 추가하고 연산 코드를LOAD_CONSTANT로 대체합니다. - 언제 작동합니까?
모듈을 처음 가져올 때 파이썬은 바이트코드를 컴파일하고 바인딩 최적화를 실행합니다. 이후의 가져오기에서는 이전 작업을 재사용하기만 합니다. 각 세션에서 이 과정을 반복합니다(결과는
pyc파일에 저장되지 않습니다). - 이것이 작동한다는 것을 어떻게 알 수 있습니까?
제가 이를 구현하여 라이브러리의 모든 모듈에 적용했으며, 테스트 모음은 예외 없이 실행되었습니다.
- 모듈이 내장 이름을 가리는 변수를 정의하면 어떻게 됩니까?
실제로 그런 일이 발생합니다. 예를 들어 모듈 수준에서 True를
True = (1==1)로 재정의할 수 있습니다. 아래의 샘플 구현은 이름 겹침을 감지하고 전역 검색을 변경하지 않은 채로 둡니다. - 대부분의 전역 검색이 결코 변하지 않는 값을 대상으로 한다는 것을 알아낸 최초의 사람입니까?
아닙니다. 이것은 오래전부터 알려져 왔습니다. Skip Montanaro가 PEP 266에서 설득력 있게 설명합니다.
- builtins 모듈을 대체하고 직접 구현한 항목을 제공하려면 어떻게 합니까?
모듈을 가져오기 전에 이를 수행하거나, 모듈을 다시 로드하거나,
codetweaks.py를 비활성화하십시오(비활성화 플래그가 있습니다). - 이 모듈은 파이썬의 바이트코드 생성 변경에 얼마나 영향을 받습니까?
번호가 다시 매겨지는 것을 방지하기 위해
opcode.py를 가져옵니다. 또한 기본적인 기능이며 아주 오래전부터 존재해 온LOAD_CONST와LOAD_GLOBAL을 사용합니다. 그렇기는 하지만 코딩 방식이 변경될 수 있으며, 이 구현도 현재 코딩 방식에 의존하는dis같은 모듈과 함께 변경되어야 합니다. - 시작 시간에 미치는 영향은 무엇입니까?
차이를 측정할 수 없었습니다. warnings.py를 제외한 시작 모듈은 어느 것도 바인딩되지 않습니다. 또한 바인딩 함수는 매우 빨라서
LOAD_GLOBAL연산 코드를 찾기 위해 코드 문자열을 단 한 번만 순회합니다.
샘플 구현
다음은 codetweaks.py의 샘플 구현입니다.:
from types import ClassType, FunctionType
from opcode import opmap, HAVE_ARGUMENT, EXTENDED_ARG
LOAD_GLOBAL, LOAD_CONST = opmap['LOAD_GLOBAL'], opmap['LOAD_CONST']
ABORT_CODES = (EXTENDED_ARG, opmap['STORE_GLOBAL'])
def bind_constants(f, builtin_only=False, stoplist=[], verbose=False):
""" Return a new function with optimized global references.
Replaces global references with their currently defined values.
If not defined, the dynamic (runtime) global lookup is left undisturbed.
If builtin_only is True, then only builtins are optimized.
Variable names in the stoplist are also left undisturbed.
If verbose is True, prints each substitution as is occurs.
"""
import __builtin__
env = vars(__builtin__).copy()
stoplist = dict.fromkeys(stoplist)
if builtin_only:
stoplist.update(f.func_globals)
else:
env.update(f.func_globals)
co = f.func_code
newcode = map(ord, co.co_code)
newconsts = list(co.co_consts)
codelen = len(newcode)
i = 0
while i < codelen:
opcode = newcode[i]
if opcode in ABORT_CODES:
return f # for simplicity, only optimize common cases
if opcode == LOAD_GLOBAL:
oparg = newcode[i+1] + (newcode[i+2] << 8)
name = co.co_names[oparg]
if name in env and name not in stoplist:
value = env[name]
try:
pos = newconsts.index(value)
except ValueError:
pos = len(newconsts)
newconsts.append(value)
newcode[i] = LOAD_CONST
newcode[i+1] = pos & 0xFF
newcode[i+2] = pos >> 8
if verbose:
print name, '-->', value
i += 1
if opcode >= HAVE_ARGUMENT:
i += 2
codestr = ''.join(map(chr, newcode))
codeobj = type(co)(co.co_argcount, co.co_nlocals, co.co_stacksize,
co.co_flags, codestr, tuple(newconsts), co.co_names,
co.co_varnames, co.co_filename, co.co_name,
co.co_firstlineno, co.co_lnotab, co.co_freevars,
co.co_cellvars)
return type(f)(codeobj, f.func_globals, f.func_name, f.func_defaults,
f.func_closure)
def bind_all(mc, builtin_only=False, stoplist=[], verbose=False):
"""Recursively apply bind_constants() to functions in a module or class.
Use as the last line of the module (after everything is defined, but
before test code).
In modules that need modifiable globals, set builtin_only to True.
"""
for k, v in vars(mc).items():
if type(v) is FunctionType:
newv = bind_constants(v, builtin_only, stoplist, verbose)
setattr(mc, k, newv)
elif type(v) in (type, ClassType):
bind_all(v, builtin_only, stoplist, verbose)
def f(): pass
try:
f.func_code.code
except AttributeError: # detect non-CPython environments
bind_all = lambda *args, **kwds: 0
del f
import sys
bind_all(sys.modules[__name__]) # Optimizer, optimize thyself!
바이트코드 [2]가 없는 CPython이 아닌 환경을 자동으로 감지한다는 점에 유의하십시오. 이러한 상황에서는 bind 함수가 단순히 원래 함수를 변경하지 않고 반환합니다. 따라서 라이브러리 모듈에 두 줄을 추가해도 다른 구현에 영향을 주지 않습니다.
최종 코드는 바인딩을 쉽게 비활성화할 수 있도록 플래그를 추가해야 합니다.
참조
[1] 비공개가 아닌(non-private) 구현을 위한 ASPN 레시피 https://code.activestate.com/recipes/277940/
Copyright
This document has been placed in the public domain.