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

Python 개선 제안 한국어 번역

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는 함수에 가드가 적용된 특수화 코드를 추가하는 공개 API를 Python C 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보다 성능이 떨어지거나 성능이 불안정하다고 여전히 보고합니다.

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

Numba는 이제 AOT(ahead of time) 컴파일을 지원하지만, 인자 타입을 지정하는 데코레이터가 필요하며 숫자 타입만 지원합니다.

CPython 3.5에는 최적화가 거의 없습니다. 핍홀 최적화기는 기본적인 최적화만 구현합니다. 정적 컴파일러는 CPython 3.5와 PyPy 사이의 절충안입니다.

Note

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

예시

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

가상의 myoptimizer 모듈

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

  • specialize(func, code, guards): 가드 guards를 가진 특수화된 코드 code를 함수 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)를 호출하는 원래 바이트코드를 실행합니다.

마이크로벤치마크에서 특수화된 바이트코드를 호출하는 데는 88 ns가 걸리는 반면, 원래 함수는 145 ns(+57 ns)가 걸립니다. 즉, 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

첫 번째 호출은 (Python 프레임을 생성하지 않고) C 내장 chr() 함수를 호출합니다. 두 번째 호출은 내장 chr() 함수가 교체되었기 때문에 특수화된 코드를 제거하고, 원래 바이트코드를 실행합니다.

마이크로벤치마크에서 C 내장 함수를 호출하는 데는 95 ns가 걸리는 반면, 원래의 바이트코드는 155 ns(+60 ns)가 걸립니다. 즉, 1.6배 빠릅니다. chr(65)를 직접 호출하면 76 ns가 걸립니다.

특수화된 코드 선택

순수 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) 튜플의 리스트를 반환합니다. 여기서 code는 호출 가능 객체 또는 코드 객체이고, guards는 PyFuncGuard 객체의 리스트입니다. 오류 시 예외를 발생시키고 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.