[Rejected] PEP 510 - Specialize functions with guards

원문 링크: PEP 510 - Specialize functions with guards

상태: Rejected 유형: Standards Track 작성일: 04-Jan-2016

PEP 510 – 가드(Guards)를 통한 함수 특수화 (Specialize functions with guards)

  • 작성자: Victor Stinner
  • 상태: 거부됨 (Rejected)
  • 유형: Standards Track
  • 작성일: 2016년 1월 4일
  • Python 버전: 3.6

거부 통지 (Rejection Notice)

이 PEP는 저자에 의해 거부되었습니다. 제안된 설계가 눈에 띄는 속도 향상을 보여주지 못했으며, 가장 발전되고 복잡한 최적화를 구현할 시간이 부족했기 때문입니다.

요약 (Abstract)

이 PEP는 순수 Python 함수를 특수화(specialize)하기 위해 Python C API에 함수를 추가하는 것을 제안합니다. 이는 가드(guards)를 사용하여 특수화된 코드(specialized codes)를 추가하는 것으로, Python의 의미론(semantics)을 준수하면서 정적 최적화 도구를 구현할 수 있게 합니다.

제안 배경 (Rationale)

Python의 의미론 (Python semantics)

Python은 거의 모든 것이 변경 가능(mutable)하기 때문에 최적화하기 어렵습니다. 내장 함수(builtin functions), 함수 코드, 전역 변수(global variables), 지역 변수(local variables) 등이 런타임에 수정될 수 있습니다. Python의 의미론을 존중하면서 최적화를 구현하려면 “무언가가 변경되었을 때”를 감지해야 하며, 이러한 검사를 “가드(guards)”라고 부릅니다.

이 PEP는 함수에 가드를 가진 특수화된 코드를 추가하기 위한 공개 API를 Python C API에 추가할 것을 제안합니다. 함수가 호출될 때, 아무것도 변경되지 않았다면 특수화된 코드가 사용되고, 그렇지 않으면 원래의 바이트코드(bytecode)가 사용됩니다.

가드가 Python의 의미론 대부분을 준수하는 데 도움이 되지만, 정확한 동작에 미묘한 변경 없이 Python을 최적화하기는 어렵습니다. CPython은 오랜 역사를 가지고 있으며 많은 애플리케이션이 구현 세부 사항에 의존하고 있습니다. “모든 것이 변경 가능”이라는 특성과 성능 사이에서 타협점을 찾아야 합니다.

옵티마이저(optimizer)를 작성하는 것은 이 PEP의 범위를 벗어납니다.

왜 JIT 컴파일러(JIT compiler)가 아닌가?

현재 활발히 개발 중인 여러 Python JIT 컴파일러가 있습니다:

  • PyPy
  • Pyston
  • Numba
  • Pyjion

Numba는 수치 계산에 특화되어 있습니다. Pyston과 Pyjion은 아직 초기 단계입니다. PyPy는 가장 완벽한 Python 인터프리터로, 일반적으로 마이크로-벤치마크와 많은 매크로-벤치마크에서 CPython보다 빠르며 CPython과의 호환성이 매우 좋습니다 (Python 의미론을 존중합니다). 그럼에도 불구하고 Python JIT 컴파일러에는 CPython 대신 널리 사용되지 못하게 하는 문제점들이 있습니다.

numpy, PyGTK, PyQt, PySide, wxPython과 같은 많은 인기 라이브러리는 C 또는 C++로 구현되었으며 Python C API를 사용합니다. 작은 메모리 사용량(memory footprint)과 더 나은 성능을 위해, Python JIT 컴파일러는 더 빠른 가비지 컬렉터(garbage collector)를 사용하기 위해 레퍼런스 카운팅(reference counting)을 사용하지 않으며, 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 호환성 Wiki 참조). Python C API의 불완전한 지원이 이 문제의 일부입니다. 또한 PyPy와 CPython 사이에는 레퍼런스 카운팅과 같은 미묘한 차이가 있습니다. 예를 들어, 객체 소멸자(destructors)는 PyPy에서 항상 호출되지만, CPython보다 “나중에” 호출될 수 있습니다. 컨텍스트 매니저(context managers)를 사용하면 리소스 해제 시점을 제어하는 데 도움이 됩니다.

PyPy가 광범위한 벤치마크에서 CPython보다 훨씬 빠르지만, 일부 사용자는 특정 사용 사례에서 CPython보다 성능이 떨어지거나 불안정한 성능을 보고하기도 합니다.

Python이 1분 미만으로 실행되는 프로그램의 스크립팅 언어로 사용될 때, JIT 컴파일러는 시작 시간이 더 길고 코드를 최적화하는 데 시간이 걸리기 때문에 더 느릴 수 있습니다. 예를 들어, 대부분의 Mercurial 명령어는 몇 초밖에 걸리지 않습니다.

Numba는 이제 선행 컴파일(ahead of time compilation)을 지원하지만, 인자(arguments) 타입을 지정하기 위해 데코레이터(decorator)가 필요하며 수치 타입만 지원합니다.

CPython 3.5는 거의 최적화가 없습니다. 피프홀 옵티마이저(peephole optimizer)는 기본적인 최적화만 구현합니다. 정적 컴파일러(static compiler)는 CPython 3.5와 PyPy 사이의 타협점입니다.

참고: Unladen Swallow 프로젝트도 있었지만, 2011년에 중단되었습니다.

예시 (Examples)

이 예시들은 중요한 속도 향상을 약속하는 강력한 최적화를 보여주기 위함이 아니라, 원리를 설명하기 위해 짧고 이해하기 쉽게 작성되었습니다.

가상의 myoptimizer 모듈 (Hypothetical myoptimizer module)

이 PEP의 예시들은 다음 함수와 타입을 제공하는 가상의 myoptimizer 모듈을 사용합니다:

  • specialize(func, code, guards): 함수 func에 가드 guards를 가진 특수화된 코드 code를 추가합니다.
  • get_specialized(func): 특수화된 코드 목록을 (code, guards) 튜플 리스트로 가져옵니다. 여기서 code는 호출 가능(callable) 객체 또는 코드 객체이며, guards는 가드 리스트입니다.
  • GuardBuiltins(name): builtins.__dict__[name]globals()[name]을 감시하는 가드입니다. builtins.__dict__[name]이 교체되거나 globals()[name]이 설정되면 가드는 실패합니다.

바이트코드 사용 (Using bytecode)

순수 내장 함수 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)))

import builtins
builtins.chr = lambda obj: "mock" # builtins.chr 변경

print("func(): %s" % func())
print("#specialized: %s" % len(myoptimizer.get_specialized(func)))

출력:

func(): A
#specialized: 1

func(): mock
#specialized: 0

첫 번째 호출은 문자열 "A"를 반환하는 특수화된 바이트코드를 사용합니다. 두 번째 호출은 내장 chr() 함수가 교체되었기 때문에 특수화된 코드를 제거하고, chr(65)를 호출하는 원래 바이트코드를 실행합니다.

마이크로-벤치마크에서 특수화된 바이트코드 호출은 88 ns가 걸렸고, 원본 함수는 145 ns (+57 ns)가 걸렸습니다. 즉, 1.6배 빨랐습니다.

내장 함수 사용 (Using builtin function)

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)))

import builtins
builtins.chr = lambda obj: "mock" # builtins.chr 변경

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 내장 함수 호출은 95 ns가 걸렸고, 원래 바이트코드는 155 ns (+60 ns)가 걸렸습니다. 즉, 1.6배 빨랐습니다. chr(65)를 직접 호출하는 것은 76 ns가 걸립니다.

특수화된 코드 선택 (Choose the specialized code)

순수 Python 함수를 호출하기 위해 특수화된 코드를 선택하는 의사 코드(pseudo-code):

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: # 모든 가드가 성공: 특수화된 코드를 사용
            return specialized_code
        elif check == 1: # 가드가 일시적으로 실패: 다음 특수화된 코드를 시도
            index += 1
        else: # assert check == 2: 가드가 항상 실패: 특수화된 코드를 제거
            del specialized[index]
    # 각 특수화된 코드의 가드가 실패했거나, 함수에 특수화된 코드가 없는 경우,
    # 원래 바이트코드 사용
    code = func.__code__
    # ...

변경 사항 (Changes)

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(stable ABI)에서 명시적으로 제외됩니다.

함수 코드가 교체될 때 (func.__code__ = new_code), 모든 특수화된 코드와 가드는 제거됩니다.

함수 가드 (Function guard)

함수 가드 객체를 추가합니다:

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은 인자(arguments)의 배열입니다: 인덱스된 인자들 다음에 키워드 인자들의 (key, value) 쌍이 옵니다. na는 인덱스된 인자들의 개수입니다. nk는 키워드 인자들의 개수입니다 ( (key, value) 쌍의 개수). stackna + nk * 2개의 객체를 포함합니다.

특수화된 코드 (Specialized code)

특수화된 코드 구조체를 추가합니다:

typedef struct {
    PyObject *code; /* 호출 가능(callable) 객체 또는 코드 객체 */
    Py_ssize_t nb_guard;
    PyObject **guards; /* PyFuncGuardObject 객체들 */
} PySpecializedCode;

함수 메서드 (Function methods)

  • PyFunction_Specialize(PyObject *func, PyObject *code, PyObject *guards) 함수를 특수화하고, 가드를 가진 특수화된 코드를 추가하는 함수 메서드입니다. code가 Python 함수인 경우, code 함수의 코드 객체가 특수화된 코드로 사용됩니다. 특수화된 Python 함수는 동일한 기본 매개변수(parameter defaults)와 키워드 매개변수 기본값을 가져야 하며, 특수화된 코드를 가져서는 안 됩니다. code가 Python 함수 또는 코드 객체인 경우, 새로운 코드 객체가 생성되고 func의 코드 객체의 코드 이름과 첫 번째 줄 번호가 복사됩니다. 특수화된 코드는 동일한 Cell 변수(cell variables)와 Free 변수(free variables)를 가져야 합니다. 결과:
    • 성공 시 0을 반환합니다.
    • 특수화가 무시된 경우 1을 반환합니다.
    • 오류 발생 시 예외를 발생시키고 -1을 반환합니다.
  • PyFunction_GetSpecializedCodes(PyObject *func) 특수화된 코드 목록을 가져오는 함수 메서드입니다. code가 호출 가능 객체 또는 코드 객체이고 guardsPyFuncGuard 객체 목록인 (code, guards) 튜플 리스트를 반환합니다. 오류 발생 시 예외를 발생시키고 NULL을 반환합니다.

  • PyFunction_GetSpecializedCode(PyObject *func, PyObject **stack, int na, int nk) 가드를 검사하여 특수화된 코드를 선택하는 함수 메서드입니다. stack, na, nk 인자에 대해서는 가드의 check() 함수를 참조하십시오. 성공 시 호출 가능 객체 또는 코드 객체를 반환합니다. 오류 발생 시 예외를 발생시키고 NULL을 반환합니다.

  • PyFunction_RemoveSpecialized(PyObject *func, Py_ssize_t index) 인덱스로 특수화된 코드와 해당 가드를 제거하는 함수 메서드입니다. 성공 시 또는 인덱스가 존재하지 않을 경우 0을 반환합니다. 오류 발생 시 예외를 발생시키고 -1을 반환합니다.

  • PyFunction_RemoveAllSpecialized(PyObject *func) 함수의 모든 특수화된 코드와 가드를 제거하는 함수 메서드입니다. 성공 시 0을 반환합니다. func가 함수가 아닐 경우 예외를 발생시키고 -1을 반환합니다.

벤치마크 (Benchmark)

python3.6 -m timeit -s 'def f(): pass' 'f()' 에 대한 마이크로-벤치마크 (3회 실행 중 최적):

  • 원본 Python: 79 ns
  • 패치된 Python: 79 ns

이 마이크로-벤치마크에 따르면, 특수화 없이 Python 함수를 호출하는 데는 오버헤드가 없습니다.

구현 (Implementation)

이 PEP를 구현하는 패치는 이슈 #26098: PEP 510: Specialize functions with guards에 포함되어 있습니다.

다른 Python 구현체 (Other implementations of Python)

이 PEP는 Python C API에 대한 변경 사항만 포함하며, Python API는 변경되지 않습니다. 다른 Python 구현체는 새로운 추가 사항을 구현하지 않거나, 추가된 함수를 no-op (아무 작업도 하지 않는)으로 구현할 자유가 있습니다. 예를 들면 다음과 같습니다:

  • PyFunction_Specialize(): 항상 1을 반환합니다 (특수화가 무시됨).
  • PyFunction_GetSpecializedCodes(): 항상 빈 리스트를 반환합니다.
  • PyFunction_GetSpecializedCode(): 기존의 PyFunction_GET_CODE() 매크로처럼 함수 코드 객체를 반환합니다.

논의 (Discussion)

python-ideas 메일링 리스트의 스레드: RFC: PEP: Specialized functions with guards.

이 문서는 퍼블릭 도메인(public domain)에 있습니다.

⚠️ 알림: 이 문서는 AI를 활용하여 번역되었으며, 기술적 정확성을 보장하지 않습니다. 정확한 내용은 반드시 원문을 확인하시기 바랍니다.

Comments