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

Python 개선 제안 한국어 번역

PEP 590 – Vectorcall: CPython을 위한 고속 호출 프로토콜

Author:
Mark Shannon <mark at hotpy.org>, Jeroen Demeyer <J.Demeyer at UGent.be>
BDFL-Delegate:
Petr Viktorin <encukou at gmail.com>
Status:
Final
Type:
Standards Track
Created:
29-Mar-2019
Python-Version:
3.8
Post-History:


Table of Contents

번역·라이선스 안내

이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판

Important

This PEP is a historical document. The up-to-date, canonical documentation can now be found at The Vectorcall Protocol.

×

See PEP 1 for how to propose changes.

초록

이 PEP는 객체 호출을 최적화하기 위한 새로운 C API를 도입합니다. 새로운 “vectorcall” 프로토콜과 호출 규약을 도입합니다. 이는 CPython 내부에서 이미 사용되는 “fastcall” 규약을 기반으로 합니다. 새로운 기능은 모든 사용자 정의 확장 클래스에서 사용할 수 있습니다.

새로운 API 대부분은 CPython 3.8에서 비공개입니다. Python 3.9에서 의미를 확정하고 공개하는 것이 계획입니다.

NOTE: 이 PEP는 Python/C API만 다루며, Python 언어나 표준 라이브러리에는 영향을 주지 않습니다.

동기

호출 규약의 선택은 호출 양쪽 코드의 성능과 유연성에 영향을 미칩니다. 성능과 유연성 사이에는 긴장이 발생하는 경우가 많습니다.

현재 tp_call [2] 호출 규약은 모든 경우를 처리할 만큼 충분히 유연하지만, 성능이 낮습니다. 낮은 성능은 호출 중간에 중간 튜플과 경우에 따라 중간 딕셔너리를 생성해야 하기 때문에 발생하는 경우가 대부분입니다. CPython에서는 Python 함수와 내장 함수 호출을 빠르게 처리하는 특수 사례 코드를 포함하여 이를 완화합니다. 안타깝게도 이는 클래스나 서드 파티 확장 객체와 같은 다른 호출 가능 객체가 더 느리고 일반적인 tp_call 호출 규약을 사용하여 호출된다는 의미입니다.

이 PEP는 Python 함수와 내장 함수에 내부적으로 사용되는 호출 규약을 일반화하고 공개하여 모든 호출이 더 나은 성능의 이점을 얻을 수 있도록 제안합니다. 새로 제안된 호출 규약은 완전히 일반적이지는 않지만, 대부분의 호출을 처리합니다. 이는 임시 객체 생성과 여러 단계의 간접 참조에 따른 오버헤드를 제거하도록 설계되었습니다.

tp_call 규약의 또 다른 비효율성은 객체별이 아니라 클래스별로 함수 포인터를 하나씩 둔다는 점입니다. 여러 중간 객체를 생성해야 하므로 클래스 호출에서는 이것이 비효율적입니다. cls 클래스의 경우 type.__call__, cls.__new__, cls.__init__ 순서의 각 호출마다 하나 이상의 중간 객체가 생성됩니다.

이 PEP는 확장 모듈에서 사용할 인터페이스를 제안합니다. 이러한 인터페이스는 소비자가 개발 과정에 참여하지 않으면 효과적으로 테스트하거나 설계할 수 없습니다. 이러한 이유로 비공개(밑줄 접두사가 붙은) 이름을 제공합니다. Python 3.9에서는 소비자 의견을 바탕으로 API가 변경될 수 있으며, 이때 API가 확정되고 밑줄이 제거될 것으로 예상합니다.

명세

함수 포인터 타입

호출은 다음 매개변수를 받는 함수 포인터를 통해 이루어집니다.

  • PyObject *callable: 호출 대상 객체
  • PyObject *const *args: 인자 벡터
  • size_t nargs: 인수 개수에 선택적 플래그 PY_VECTORCALL_ARGUMENTS_OFFSET을 더한 값(아래 참조)
  • PyObject *kwnames: NULL이거나 키워드 인자의 이름을 포함하는 튜플입니다.

다음 함수 포인터 형식으로 구현됩니다: typedef PyObject *(*vectorcallfunc)(PyObject *callable, PyObject *const *args, size_t nargs, PyObject *kwnames);

PyTypeObject 구조체의 변경 사항입니다.

사용되지 않는 printfunc tp_print 슬롯이 tp_vectorcall_offset로 대체됩니다. 그 형식은 Py_ssize_t입니다. 새로운 tp_flags 플래그인 _Py_TPFLAGS_HAVE_VECTORCALL이 추가되며, 벡터콜 프로토콜을 사용하는 모든 클래스에 설정해야 합니다.

_Py_TPFLAGS_HAVE_VECTORCALL이 설정된 경우 tp_vectorcall_offset은 양의 정수여야 합니다. 이는 vectorcallfunc 형식인 벡터콜 함수 포인터의 객체 내 오프셋입니다. 이 포인터는 NULL일 수 있으며, 이 경우의 동작은 _Py_TPFLAGS_HAVE_VECTORCALL이 설정되지 않은 경우와 같습니다.

외부 프로젝트가 이전 Python 버전에 벡터콜 프로토콜을 백포트하기 쉽도록 tp_print 슬롯을 tp_vectorcall_offset 슬롯으로 재사용합니다. 특히 Cython 프로젝트가 이를 수행하는 데 관심을 보였습니다(https://mail.python.org/pipermail/python-dev/2018-June/153927.html 참조).

디스크립터 동작입니다.

추가 형식 플래그 하나가 지정됩니다: Py_TPFLAGS_METHOD_DESCRIPTOR.

호출 가능 객체가 디스크립터 프로토콜을 사용하여 바운드 메서드와 유사한 객체를 생성하는 경우 Py_TPFLAGS_METHOD_DESCRIPTOR를 설정해야 합니다. 이는 인터프리터가 메서드를 호출할 때 임시 객체를 생성하지 않도록 하기 위해 사용됩니다(_PyObject_GetMethodLOAD_METHOD/CALL_METHOD 연산 코드 참조).

구체적으로 type(func)Py_TPFLAGS_METHOD_DESCRIPTOR가 설정되어 있으면 다음과 같습니다.

  • obj가 None이 아닌 경우 func.__get__(obj, cls)(*args, **kwds)func(obj, *args, **kwds)와 동등해야 합니다.
  • func.__get__(None, cls)(*args, **kwds)func(*args, **kwds)와 동등해야 합니다.

func.__get__(obj, cls) 객체에는 어떠한 제한도 없습니다. 후자는 벡터콜 프로토콜을 구현할 필요가 없습니다.

호출입니다.

호출은 ((vectorcallfunc)(((char *)o)+offset))(o, args, n, kwnames)의 형태를 취하며, 여기서 offsetPy_TYPE(o)->tp_vectorcall_offset입니다. 호출자는 kwnames 튜플을 생성하고 그 안에 중복 항목이 없도록 할 책임이 있습니다.

n은 위치 인수의 개수에 PY_VECTORCALL_ARGUMENTS_OFFSET 플래그가 추가될 수도 있는 값입니다.

PY_VECTORCALL_ARGUMENTS_OFFSET

피호출자가 args[-1]을 일시적으로 변경할 수 있는 경우 PY_VECTORCALL_ARGUMENTS_OFFSET 플래그를 n에 추가해야 합니다. 다시 말해, 이는 args가 할당된 벡터의 인수 1을 가리키는 경우에 사용할 수 있습니다. 피호출자는 반환하기 전에 args[-1]의 값을 복원해야 합니다.

호출자는 추가 할당 없이 저렴하게 수행할 수 있는 경우 PY_VECTORCALL_ARGUMENTS_OFFSET을 사용하도록 권장됩니다. 이렇게 하면 바운드 메서드와 같은 호출 가능 객체가 후속 호출을 저렴하게 수행할 수 있습니다. 바이트코드 인터프리터는 이미 호출 가능 객체를 위한 스택 공간을 할당하므로 추가 비용 없이 이 방법을 사용할 수 있습니다.

PY_VECTORCALL_ARGUMENTS_OFFSET을 피호출자가 할당을 피하기 위해 사용하는 방법의 예는 [3]을 참조하십시오.

매개변수 n에서 실제 인수 개수를 가져오려면 매크로 PyVectorcall_NARGS(n)을 사용해야 합니다. 이를 통해 향후 변경이나 확장이 가능해집니다.

새로운 C API 및 CPython 변경 사항

다음 함수 또는 매크로가 C API에 추가됩니다.

  • PyObject *_PyObject_Vectorcall(PyObject *obj, PyObject *const *args, size_t nargs, PyObject *keywords): 주어진 인수로 obj를 호출합니다. nargs에 플래그 PY_VECTORCALL_ARGUMENTS_OFFSET가 포함될 수 있다는 점에 유의하십시오. 실제 위치 인수 개수는 PyVectorcall_NARGS(nargs)로 주어집니다. keywords 인수는 키워드 이름의 튜플이거나 NULL입니다. 빈 튜플은 NULL을 전달하는 것과 같은 효과를 냅니다. 내부적으로 벡터콜 프로토콜 또는 tp_call을 사용하며, 둘 다 지원되지 않으면 예외가 발생합니다.
  • PyObject *PyVectorcall_Call(PyObject *obj, PyObject *tuple, PyObject *dict): 기존 *args**kwargs호출 규약으로 객체(벡터콜을 지원해야 합니다)를 호출합니다. 이는 주로 tp_call 슬롯에 배치하기 위한 것입니다.
  • Py_ssize_t PyVectorcall_NARGS(size_t nargs): 벡터콜 nargs 인수가 주어지면 실제 인수 개수를 반환합니다. 현재는 nargs & ~PY_VECTORCALL_ARGUMENTS_OFFSET와 동등합니다.

서브클래싱

확장 타입은 기본 클래스와 동일한 방식으로 tp_call을 구현하는 경우 기본 클래스에서 타입 플래그 _Py_TPFLAGS_HAVE_VECTORCALL과 값 tp_vectorcall_offset을 상속합니다. 또한 tp_descr_get이 기본 클래스와 동일한 방식으로 구현되어 있으면 플래그 Py_TPFLAGS_METHOD_DESCRIPTOR도 상속됩니다.

힙 타입은 벡터콜 프로토콜을 절대 상속하지 않습니다. 이는 안전하지 않기 때문입니다(힙 타입은 동적으로 변경될 수 있습니다). 향후 이 제한이 해제될 수 있지만, 그러려면 type.__setattribute__에서 __call__을 특별히 처리해야 합니다.

API 마무리

_PyObject_Vectorcall_Py_TPFLAGS_HAVE_VECTORCALL이라는 이름의 밑줄은 이 API가 Python 마이너 버전에서 변경될 수 있음을 나타냅니다. 마무리될 때(이는 Python 3.9에서 진행할 계획입니다) 이 이름은 PyObject_VectorcallPy_TPFLAGS_HAVE_VECTORCALL로 변경됩니다. 기존의 밑줄 접두사가 붙은 이름은 별칭으로 계속 사용할 수 있습니다.

새로운 API는 일반적인 방식으로 문서화되지만, 위 사항에 대한 경고가 포함됩니다.

이 PEP에서 도입된 다른 이름들(PyVectorcall_NARGS, PyVectorcall_Call, Py_TPFLAGS_METHOD_DESCRIPTOR, PY_VECTORCALL_ARGUMENTS_OFFSET)의 의미는 확정되었습니다.

CPython 내부 변경 사항

기존 클래스 변경 사항

function, builtin_function_or_method, method_descriptor, method, wrapper_descriptor, method-wrapper 클래스는 벡터콜 프로토콜을 사용합니다(초기 구현에서 이들 모두가 변경되지는 않습니다).

PyMethodDef 데이터 구조를 사용하는 builtin_function_or_methodmethod_descriptor에 대해서는 기존의 각 호출 규약에 맞는 특정 벡터콜 래퍼를 구현할 수 있습니다. 그렇게 하는 것이 가치가 있을지는 아직 지켜봐야 합니다.

클래스에 벡터콜 프로토콜 사용하기

클래스 cls의 경우, cls(xxx)를 사용하여 새 인스턴스를 생성하려면 여러 번 호출해야 합니다. type.__call__, cls.__new__, cls.__init__의 호출 시퀀스에서 각 호출마다 적어도 하나의 중간 객체가 생성됩니다. 따라서 클래스를 호출할 때 vectorcall을 사용하는 것은 매우 합리적입니다. 이는 실제로 type에 vectorcall 프로토콜을 구현한다는 의미입니다. 가장 자주 사용되는 클래스 중 일부는 이 프로토콜을 사용하게 될 것이며, 아마도 range, list, strtype일 것입니다.

PyMethodDef 프로토콜 및 Argument Clinic

Argument Clinic [4]은 하위 수준 호출 가능 객체를 둘러싸는 래퍼 함수를 자동으로 생성하여 기본 타입의 안전한 언박싱과 기타 안전성 검사를 제공합니다. Argument Clinic을 확장하여 새로운 vectorcall 프로토콜을 따르는 래퍼 객체를 생성할 수 있습니다. 이를 통해 단 한 번의 간접 참조만으로 호출자에서 Argument Clinic이 생성한 래퍼로, 이어서 손으로 작성한 코드로 실행이 전달될 수 있습니다.

vectorcall을 사용하는 서드파티 확장 클래스

Python 함수 및 내장 함수와 동등한 호출 성능을 구현하려면, 서드파티 호출 가능 객체는 vectorcallfunc함수 포인터를 포함하고, tp_vectorcall_offset을 올바른 값으로 설정하며, _Py_TPFLAGS_HAVE_VECTORCALL플래그를 추가해야 합니다. 이렇게 하는 모든 클래스는 tp_call함수를 구현하고 그 동작이 vectorcallfunc함수와 일관되도록 해야 합니다. tp_callPyVectorcall_Call로 설정하는 것으로 충분합니다.

이러한 변경 사항이 성능에 미치는 영향

이 PEP는 기존 코드의 성능에 큰 영향을 미치지 않아야 합니다(긍정적인 의미에서도 부정적인 의미에서도 그렇습니다). 이 PEP의 주된 목적은 효율적인 새 코드를 작성할 수 있도록 하는 것이며, 기존 코드를 더 빠르게 만드는 것이 아닙니다.

그럼에도 이 PEP는 METH_FASTCALL함수를 대상으로 최적화합니다. METH_VARARGS를 사용하는 함수의 성능은 약간 저하됩니다.

안정 ABI

이 PEP의 어떤 내용도 안정 ABI (PEP 384)에 추가되지 않습니다.

대안 제안

bpo-29259

PEP 590은 bpo-29259 [1] 에서 제안된 내용과 거의 같습니다. 주요 차이점은 이 PEP가 함수 포인터를 클래스가 아니라 인스턴스에 저장한다는 점입니다. 이는 각 인스턴스가 서로 다른 C 함수에 대응하는 C 함수 구현에 더 적합합니다. 또한 bpo-29259로는 불가능한 type.__call__최적화도 가능하게 합니다.

PEP 576 및 PEP 580

관련 PEP 576PEP 580은 모두 서드 파티 객체가 표현력이 풍부하면서도 높은 성능을 발휘하도록(CPython 객체와 동등한 수준으로) 설계되었습니다. 이 PEP의 목적은 CPython 생태계의 객체를 표현력이 높고 가능한 한 성능이 뛰어난 방식으로 호출할 수 있는 통일된 방법을 제공하는 것입니다.

이 PEP는 PEP 576보다 범위가 넓으며, 고정 오프셋 함수 포인터가 아니라 가변 오프셋 함수 포인터를 사용합니다. 기반 호출 규약은 유사합니다. 함수 포인터에 고정 오프셋만 허용하기 때문에, PEP 576은 레이아웃에 제약이 있는 객체에 개선 사항을 적용할 수 없습니다.

PEP 580은 내장 함수를 정의하는 데 사용되는 PyMethodDef 프로토콜에 대한 주요 변경을 제안합니다. 이 PEP는 새로운 호출 규약의 형태로 더 일반적이고 간단한 메커니즘을 제공합니다. 이 PEP는 PyMethodDef 프로토콜도 확장하지만, 기존 규약을 공식화할 뿐입니다.

기타 거부된 접근 방식

벡터 인자와 선택적 튜플 및 딕셔너리 인자를 모두 결합한 더 긴 6인자 형식이 고려되었습니다. 그러나 이 형식과 기존 tp_call 형식 간에 변환하는 코드가 지나치게 번거롭고 비효율적이라는 사실이 밝혀졌습니다. 또한 x64 Windows에서는 레지스터를 통해 전달되는 인자가 4개뿐이므로, 추가되는 2개의 인자에는 무시할 수 없는 비용이 발생합니다.

모든 특수한 경우를 제거하고 모든 호출에서 tp_call 형식을 사용하도록 하는 방안도 고려되었습니다. 그러나 튜플을 생성하고 제거하는 훨씬 더 효율적인 방법을 찾지 못한다면, 딕셔너리의 경우에는 그 정도가 덜하지만, 너무 느릴 것입니다.

감사의 말

CPython 내부에서 원래의 “fastcall” 호출 규약을 개발한 Victor Stinner에게 감사드립니다. 이 PEP는 그의 작업을 성문화하고 확장합니다.

참고 자료

참조 구현

최소 구현은 https://github.com/markshannon/cpython/tree/vectorcall-minimal 에서 찾을 수 있습니다.