PEP 510 – 가드를 사용한 함수 특수화
- Author:
- Victor Stinner <vstinner at python.org>
- Status:
- Rejected
- Type:
- Standards Track
- Created:
- 04-Jan-2016
- Python-Version:
- 3.6
Table of Contents
번역·라이선스 안내
이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판
거부 통지
이 PEP는 설계가 유의미한 속도 향상을 보여 주지 못했으며, 가장 진보되고 복잡한 최적화를 구현할 시간이 부족했기 때문에 작성자에 의해 거부되었습니다.
초록
순수 Python 함수를 특수화하도록 Python C API에 함수를 추가합니다. 가드가 적용된 특수화 코드를 추가합니다. 이를 통해 Python 의미론을 준수하는 정적 최적화기를 구현할 수 있습니다.
근거
Python 의미론
거의 모든 것이 변경 가능하기 때문에 Python은 최적화하기 어렵습니다. 내장 함수, 함수 코드, 전역 변수, 지역 변수 등을 실행 중에 수정할 수 있습니다. Python 의미론을 준수하는 최적화를 구현하려면 “무언가 변경되는” 시점을 감지해야 하며, 이러한 검사를 “가드”라고 부릅니다.
이 PEP는 함수에 가드가 적용된 특수화 코드를 추가할 수 있도록 Python C API에 공개 API를 추가할 것을 제안합니다. 함수를 호출할 때 변경된 사항이 없으면 특수화 코드를 사용하고, 그렇지 않으면 원래 바이트코드를 사용합니다.
가드는 Python 의미론의 대부분을 준수하는 데 도움이 되지만, 정확한 동작에 미묘한 변경을 가하지 않고 Python을 최적화하기는 어렵습니다. CPython은 오랜 역사를 지니고 있으며 많은 애플리케이션이 구현 세부 사항에 의존합니다. “모든 것이 변경 가능함”과 성능 사이에서 절충점을 찾아야 합니다.
최적화기를 작성하는 것은 이 PEP의 범위에 속하지 않습니다.
JIT 컴파일러는 왜 사용하지 않습니까?
Python용 JIT 컴파일러는 여러 개가 활발히 개발되고 있습니다.
Numba는 수치 계산에 특화되어 있습니다. Pyston과 Pyjion은 아직 초기 단계입니다. PyPy는 가장 완성도가 높은 Python 인터프리터이며, 일반적으로 마이크로 벤치마크와 여러 매크로 벤치마크에서 CPython보다 빠르고 CPython과의 호환성도 매우 뛰어납니다(Python 의미론을 준수합니다). Python JIT 컴파일러에는 여전히 CPython 대신 널리 사용되지 못하게 하는 문제가 있습니다.
numpy, PyGTK, PyQt, PySide, wxPython과 같은 많은 인기 라이브러리는 C 또는 C++로 구현되며 Python C API를 사용합니다. 메모리 사용량을 줄이고 성능을 향상하기 위해 Python JIT 컴파일러는 더 빠른 가비지 수집기를 사용하려고 참조 카운팅을 사용하지 않으며, CPython 객체의 C 구조체를 사용하지 않고 메모리 할당을 다르게 관리합니다. PyPy에는 Python C API를 에뮬레이트하는 cpyext 모듈이 있지만 CPython보다 성능이 떨어지며 Python C API 전체를 지원하지 않습니다.
새로운 기능은 CPython에서 먼저 개발됩니다. 2016년 1월 기준으로 최신 CPython 안정 버전은 3.5인 반면, PyPy는 Python 2.7과 3.2만 지원하고 Pyston은 Python 2.7만 지원합니다.
PyPy가 Python과 매우 높은 호환성을 갖추고 있더라도 일부 모듈은 여전히 PyPy와 호환되지 않습니다. PyPy Compatibility Wiki를 참조하십시오. Python C API에 대한 지원이 불완전한 것이 이 문제의 일부입니다. 참조 카운팅과 같이 PyPy와 CPython 사이에는 미묘한 차이도 있습니다. 객체 소멸자는 PyPy에서 항상 호출되지만 CPython에서보다 “나중에” 호출될 수 있습니다. 컨텍스트 관리자를 사용하면 리소스가 해제되는 시점을 제어하는 데 도움이 됩니다.
PyPy가 광범위한 벤치마크에서 CPython보다 훨씬 빠르더라도 일부 사용자는 특정 사용 사례에서 CPython보다 성능이 나쁘거나 성능이 불안정하다고 여전히 보고합니다.
Python을 1분 미만으로 실행되는 프로그램의 스크립팅 언어로 사용할 때 JIT 컴파일러는 시작 시간이 더 길고 코드를 최적화하는 데 시간이 걸리므로 더 느릴 수 있습니다. 예를 들어 대부분의 Mercurial 명령은 몇 초가 걸립니다.
Numba는 이제 사전 컴파일을 지원하지만 인자 타입을 지정하려면 데코레이터가 필요하며 수치 타입만 지원합니다.
CPython 3.5에는 최적화가 거의 없습니다. 피프홀 최적화 프로그램은 기본적인 최적화만 구현합니다. 정적 컴파일러는 CPython 3.5와 PyPy 사이의 절충안입니다.
Note
Unladen Swallow 프로젝트도 있었지만 2011년에 중단되었습니다.
예제
다음 예제는 상당한 속도 향상을 약속하는 강력한 최적화를 보여 주기 위한 것이 아니라, 원리를 설명하기 위해 짧고 이해하기 쉽게 작성되었습니다.
가상의 myoptimizer 모듈
이 PEP의 예제에서는 다음 함수와 타입을 제공하는 가상의 myoptimizer 모듈을 사용합니다.
specialize(func, code, guards): 특수화된 코드code와 가드guards를 함수func에 추가합니다.get_specialized(func): 특수화된 코드의 목록을 반환합니다. 이 목록은code가 호출 가능 객체 또는 코드 객체이고guards가 가드 목록인(code, guards)튜플의 리스트입니다.GuardBuiltins(name):builtins.__dict__[name]와globals()[name]을 감시하는 가드입니다.builtins.__dict__[name]이 대체되거나globals()[name]이 설정되면 가드가 실패합니다.
바이트코드 사용
순수 내장 함수 chr(65) 호출을 그 결과인 "A"로 대체하는 특수화된 바이트코드를 추가합니다.:
import myoptimizer
def func():
return chr(65)
def fast_func():
return "A"
myoptimizer.specialize(func, fast_func.__code__,
[myoptimizer.GuardBuiltins("chr")])
del fast_func
가드의 동작을 보여 주는 예제:
print("func(): %s" % func())
print("#specialized: %s" % len(myoptimizer.get_specialized(func)))
print()
import builtins
builtins.chr = lambda obj: "mock"
print("func(): %s" % func())
print("#specialized: %s" % len(myoptimizer.get_specialized(func)))
출력:
func(): A
#specialized: 1
func(): mock
#specialized: 0
첫 번째 호출은 문자열 "A"를 반환하는 특수화된 바이트코드를 사용합니다. 두 번째 호출은 내장 chr() 함수가 대체되었기 때문에 특수화된 코드를 제거하고 chr(65)를 호출하는 원래 바이트코드를 실행합니다.
마이크로벤치마크에서 특수화된 바이트코드를 호출하는 데는 88ns가 걸리는 반면 원래 함수에는 145ns(+57ns)가 걸립니다. 즉, 1.6배 빠릅니다.
내장 함수 사용
chr(obj)를 호출하는 바이트코드 대신 C 내장 함수 chr()를 특수화된 코드로 추가합니다.:
import myoptimizer
def func(arg):
return chr(arg)
myoptimizer.specialize(func, chr,
[myoptimizer.GuardBuiltins("chr")])
가드의 동작을 보여 주는 예제:
print("func(65): %s" % func(65))
print("#specialized: %s" % len(myoptimizer.get_specialized(func)))
print()
import builtins
builtins.chr = lambda obj: "mock"
print("func(65): %s" % func(65))
print("#specialized: %s" % len(myoptimizer.get_specialized(func)))
출력:
func(): A
#specialized: 1
func(): mock
#specialized: 0
첫 번째 호출은 C 내장 함수 chr()를 호출합니다(Python 프레임을 생성하지 않음). 두 번째 호출은 내장 chr() 함수가 대체되었기 때문에 특수화된 코드를 제거하고 원래 바이트코드를 실행합니다.
마이크로벤치마크에서 C 내장 함수를 호출하는 데 95ns가 걸리는 반면, 원래 바이트코드는 155ns가 걸립니다(+60ns): 1.6배 빠릅니다. chr(65)를 직접 호출하면 76ns가 걸립니다.
특수화된 코드를 선택하십시오
순수 Python 함수를 호출할 특수화된 코드를 선택하는 의사 코드:
def call_func(func, args, kwargs):
specialized = myoptimizer.get_specialized(func)
nspecialized = len(specialized)
index = 0
while index < nspecialized:
specialized_code, guards = specialized[index]
for guard in guards:
check = guard(args, kwargs)
if check:
break
if not check:
# all guards succeeded:
# use the specialized code
return specialized_code
elif check == 1:
# a guard failed temporarily:
# try the next specialized code
index += 1
else:
assert check == 2
# a guard will always fail:
# remove the specialized code
del specialized[index]
# if a guard of each specialized code failed, or if the function
# has no specialized code, use original bytecode
code = func.__code__
변경 사항
Python C API 변경 사항:
PyFuncGuardObject객체와PyFuncGuard_Type타입을 추가하십시오PySpecializedCode구조체를 추가하십시오PyFunctionObject구조체에 다음 필드를 추가하십시오:Py_ssize_t nb_specialized; PySpecializedCode *specialized;
- 함수 메서드를 추가하십시오:
PyFunction_Specialize()PyFunction_GetSpecializedCodes()PyFunction_GetSpecializedCode()PyFunction_RemoveSpecialized()PyFunction_RemoveAllSpecialized()
이 함수와 타입은 Python 수준에 노출되지 않습니다.
이러한 모든 추가 사항은 안정 ABI에서 명시적으로 제외됩니다.
함수 코드를 교체하면(func.__code__ = new_code), 모든 특수화된 코드와 가드가 제거됩니다.
함수 가드
함수 가드 객체를 추가하십시오:
typedef struct {
PyObject ob_base;
int (*init) (PyObject *guard, PyObject *func);
int (*check) (PyObject *guard, PyObject **stack, int na, int nk);
} PyFuncGuardObject;
init()함수는 가드를 초기화합니다:
- 성공하면
0을 반환합니다 - 가드가 항상 실패하는 경우
1을 반환합니다:PyFunction_Specialize()는 특수화된 코드를 무시해야 합니다 - 오류가 발생하면 예외를 발생시키고
-1을 반환합니다
check()함수는 가드를 검사합니다:
- 성공하면
0을 반환합니다 - 가드가 일시적으로 실패한 경우
1을 반환합니다 - 가드가 항상 실패하는 경우
2를 반환합니다: 특수화된 코드를 제거해야 합니다 - 오류가 발생하면 예외를 발생시키고
-1을 반환합니다
stack은 인자의 배열입니다. 인덱스가 지정된 인자 뒤에 키워드 인자의 (key, value) 쌍이 옵니다. na는 인덱스가 지정된 인자의 개수입니다. nk는 키워드 인자의 개수, 즉 (key, value) 쌍의 개수입니다. stack에는 na + nk * 2개의 객체가 포함됩니다.
특수화된 코드
특수화된 코드 구조체를 추가하십시오:
typedef struct {
PyObject *code; /* callable or code object */
Py_ssize_t nb_guard;
PyObject **guards; /* PyFuncGuardObject objects */
} PySpecializedCode;
함수 메서드
PyFunction_Specialize
함수를 전문화하고, 가드가 있는 전문화된 코드를 추가하는 함수 메서드를 추가합니다.:
int PyFunction_Specialize(PyObject *func,
PyObject *code, PyObject *guards)
code가 Python 함수인 경우, code 함수의 코드 객체를 전문화된 코드로 사용합니다. 전문화된 Python 함수는 동일한 매개변수 기본값과 동일한 키워드 매개변수 기본값을 가져야 하며, 전문화된 코드를 가져서는 안 됩니다.
code가 Python 함수 또는 코드 객체인 경우, 새 코드 객체를 만들고 func의 코드 객체에서 코드 이름과 첫 번째 줄 번호를 복사합니다. 전문화된 코드는 동일한 셀 변수와 동일한 자유 변수를 가져야 합니다.
결과:
- 성공하면
0을 반환합니다. - 전문화가 무시된 경우
1을 반환합니다. - 오류가 발생하면 예외를 발생시키고
-1을 반환합니다.
PyFunction_GetSpecializedCodes
전문화된 코드 목록을 가져오는 함수 메서드를 추가합니다.:
PyObject* PyFunction_GetSpecializedCodes(PyObject *func)
code가 호출 가능 객체 또는 코드 객체이고 guards가 PyFuncGuard 객체의 목록인 (code, guards) 튜플의 목록을 반환합니다. 오류가 발생하면 예외를 발생시키고 NULL을 반환합니다.
PyFunction_GetSpecializedCode
가드를 검사하여 전문화된 코드를 선택하는 함수 메서드를 추가합니다.:
PyObject* PyFunction_GetSpecializedCode(PyObject *func,
PyObject **stack,
int na, int nk)
stack, na 및 nk 인자에 대해서는 가드의 check() 함수를 참조하십시오. 성공하면 호출 가능 객체 또는 코드 객체를 반환합니다. 오류가 발생하면 예외를 발생시키고 NULL을 반환합니다.
PyFunction_RemoveSpecialized
인덱스로 해당 가드와 함께 전문화된 코드를 제거하는 함수 메서드를 추가합니다.:
int PyFunction_RemoveSpecialized(PyObject *func, Py_ssize_t index)
성공하거나 인덱스가 존재하지 않으면 0을 반환합니다. 오류가 발생하면 예외를 발생시키고 -1을 반환합니다.
PyFunction_RemoveAllSpecialized
함수의 모든 전문화된 코드와 가드를 제거하는 함수 메서드를 추가합니다.:
int PyFunction_RemoveAllSpecialized(PyObject *func)
성공하면 0을 반환합니다. func가 함수가 아니면 예외를 발생시키고 -1을 반환합니다.
벤치마크
python3.6 -m timeit -s 'def f(): pass' 'f()'에서의 마이크로벤치마크(3회 실행 중 최상의 결과):
- 원본 Python: 79 ns
- 패치된 Python: 79 ns
이 마이크로벤치마크에 따르면, 특수화하지 않고 Python 함수를 호출하는 데 변경 사항으로 인한 오버헤드가 없습니다.
구현
issue #26098: PEP 510: Specialize functions with guards에는 이 PEP를 구현하는 패치가 포함되어 있습니다.
Python의 다른 구현
이 PEP에는 Python C API에 대한 변경 사항만 포함되어 있으며, Python API는 변경되지 않습니다. Python의 다른 구현은 새로운 추가 사항을 구현하지 않거나, 추가된 함수를 no-op으로 구현할 수 있습니다.
PyFunction_Specialize(): 항상1을 반환합니다(특수화는 무시됩니다).PyFunction_GetSpecializedCodes(): 항상 빈 리스트를 반환합니다.PyFunction_GetSpecializedCode(): 기존PyFunction_GET_CODE()매크로와 마찬가지로 함수 코드 객체를 반환합니다.
논의
python-ideas 메일링 리스트의 스레드: RFC: PEP: Specialized functions with guards.
Copyright
This document has been placed in the public domain.