PEP 670 – Python C API에서 매크로를 함수로 변환하기
- Author:
- Erlend Egeberg Aasland <erlend at python.org>, Victor Stinner <vstinner at python.org>
- Status:
- Final
- Type:
- Standards Track
- Created:
- 19-Oct-2021
- Python-Version:
- 3.11
- Post-History:
- 20-Oct-2021, 08-Feb-2022, 22-Feb-2022
- Resolution:
- Python-Dev thread
Table of Contents
번역·라이선스 안내
이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain or CC0-1.0, whichever is more permissive 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판
초록
C API의 매크로를 정적 인라인 함수 또는 일반 함수로 변환합니다. 이렇게 하면 C/C++에서 매크로의 함정을 피하고, 다른 프로그래밍 언어에서도 함수를 사용할 수 있게 됩니다.
컴파일러 경고를 피하기 위해 포인터 타입의 함수 인자를 추가 매크로를 사용하여 적절한 타입으로 캐스팅합니다. 제한된 C API 버전 3.11에서는 캐스팅하지 않습니다. 새로운 제한된 API를 선택하여 사용하는 사용자는 정확히 예상되는 타입으로 캐스팅을 추가해야 할 수 있습니다.
호환되지 않는 변경 사항이 도입되는 것을 피하기 위해 대입에서 l-value로 사용할 수 있는 매크로는 변환하지 않습니다.
근거
매크로를 사용하면 숙련된 C 개발자라도 피하기 어려운 의도하지 않은 부작용이 발생할 수 있습니다. 일부 문제는 수년 동안 알려져 왔지만, 다른 문제는 최근 Python에서 발견되었습니다. 매크로의 함정을 우회하면 매크로 코드를 읽고 유지 관리하기가 더 어려워집니다.
매크로를 함수로 변환하면 여러 가지 이점이 있습니다.
- 함수에는 매크로의 함정이 없습니다. 예를 들어 다음은 GCC 문서에 설명된 함정입니다.
- 잘못된 중첩
- 연산자 우선순위 문제
- 세미콜론 삼킴
- 부수 효과 중복
- 자기 참조 매크로
- 인자 사전 스캔
- 인자의 줄 바꿈
함수에는 매크로의 함정을 피하기 위한 다음과 같은 우회 방법이 필요하지 않으므로, 일반적으로 유사한 매크로 코드보다 읽고 유지 관리하기가 더 쉽습니다.
- 인자 주위에 괄호 추가
- 함수를 여러 줄에 걸쳐 작성하는 경우 줄 연속 문자를 사용합니다.
- 여러 표현식을 실행하기 위해 쉼표를 추가합니다.
- 여러 문을 작성하기 위해
do { ... } while (0)를 사용합니다.
- 함수의 인자 타입과 반환 타입은 명확하게 정의됩니다.
- 디버거와 프로파일러는 인라인 함수의 이름을 가져올 수 있습니다.
- 디버거는 인라인 함수에 중단점을 설정할 수 있습니다.
- 변수의 범위는 명확하게 정의됩니다.
매크로와 정적 인라인 함수를 일반 함수로 변환하면, Python을 사용하지만 매크로와 정적 인라인 함수는 사용할 수 없는 프로젝트에서도 이러한 일반 함수에 접근할 수 있게 됩니다.
사양
매크로를 정적 인라인 함수로 변환합니다.
대부분의 매크로는 정적 인라인 함수로 변환됩니다.
다음 매크로는 변환되지 않습니다:
- 객체형 매크로(즉, 괄호와 인자가 필요하지 않은 매크로)입니다. 예를 들면 다음과 같습니다:
- 빈 매크로입니다. 예:
#define Py_HAVE_CONDVAR. - 잘 정의된 형식을 갖는 상수가 더 좋더라도 값만 정의하는 매크로입니다. 예:
#define METH_VARARGS 0x0001.
- 빈 매크로입니다. 예:
- 서로 다른 C 컴파일러, C 언어 확장 또는 최신 C 기능을 위한 호환성 계층입니다. 예:
Py_GCC_ATTRIBUTE(),Py_ALWAYS_INLINE,Py_MEMCPY(). - 동작이 아니라 정의에 사용되는 매크로입니다. 예:
PyAPI_FUNC,Py_DEPRECATED,Py_PYTHON_H. - 문자열화와 연결 같은 C 전처리기 기능이 필요한 매크로입니다. 예:
Py_STRINGIFY(). - 함수로 변환할 수 없는 매크로입니다. 예:
Py_BEGIN_ALLOW_THREADS(짝이 맞지 않는}를 포함함),Py_VISIT(특정 변수 이름에 의존함), Py_RETURN_RICHCOMPARE(호출하는 함수에서 반환함)입니다. - 대입에서 l-value로 사용할 수 있는 매크로입니다. 이는 호환되지 않는 변경이며 이 PEP의 범위를 벗어납니다. 예:
PyBytes_AS_STRING(). - 코드 경로 또는 인자에 따라 반환 형식이 다른 매크로입니다.
정적 인라인 함수를 일반 함수로 변환
공개 C API의 정적 인라인 함수는 해당 함수 변경으로 인한 측정 가능한 성능 저하가 없는 경우에만 일반 함수로 변환할 수 있습니다. 성능 영향은 벤치마크를 사용하여 측정해야 합니다.
포인터 인자 캐스팅
현재 포인터를 받는 대부분의 매크로는 포인터 인자를 예상 형식으로 캐스팅합니다. 예를 들어 Python 3.6에서 Py_TYPE() 매크로는 인자를 PyObject*로 캐스팅합니다:
#define Py_TYPE(ob) (((PyObject*)(ob))->ob_type)
Py_TYPE() 매크로는 PyObject*형식을 허용하지만, PyLongObject* 및 PyDictObject*와 같은 모든 포인터 형식도 허용합니다.
함수는 강한 형식 검사를 적용하므로 한 가지 인자 형식만 허용할 수 있습니다.
기존 코드에서 컴파일러 오류와 경고가 발생하지 않도록, 매크로를 함수로 변환할 때 해당 매크로가 인자 중 하나 이상을 캐스팅하면 캐스팅을 유지하는 새 매크로가 추가됩니다. 새 매크로와 함수는 동일한 이름을 사용합니다.
Py_TYPE() 매크로를 정적 인라인 함수로 변환한 예:
static inline PyTypeObject* Py_TYPE(PyObject *ob) {
return ob->ob_type;
}
#define Py_TYPE(ob) Py_TYPE((PyObject*)(ob))
캐스트는 PyObject*뿐만 아니라 모든 포인터 타입에 대해 유지됩니다. 여기에는 void*로의 캐스트도 포함됩니다. void*로의 캐스트를 제거하면 함수가 const void* 변수를 사용하여 호출될 때 새로운 경고가 발생합니다. 예를 들어 PyUnicode_WRITE() 매크로는 data 인자를 void*로 캐스팅하므로, data에 기록하더라도 현재 const void* 타입을 허용합니다. 이 PEP에서는 이를 변경하지 않습니다.
제한 C API 버전 3.11에서는 캐스트를 피하십시오.
캐스트는 제한 C API 버전 3.11 및 이후 버전에서 제외됩니다. API 사용자가 새로운 제한 API를 선택하면 예상되는 타입을 전달하거나 캐스트를 수행해야 합니다.
예를 들어 Py_TYPE()은 다음과 같이 정의됩니다.
static inline PyTypeObject* Py_TYPE(PyObject *ob) {
return ob->ob_type;
}
#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 < 0x030b0000
# define Py_TYPE(ob) Py_TYPE((PyObject*)(ob))
#endif
반환 타입은 변경하지 않습니다.
매크로를 함수로 변환할 때 새로운 컴파일러 경고가 발생하지 않도록 반환 타입을 변경해서는 안 됩니다.
예를 들어 Python 3.7에서는 PyUnicode_AsUTF8()의 반환 타입을 char*에서 const char*로 변경했습니다 (commit). 이 변경으로 인해 char*를 예상하는 C 확장을 빌드할 때 새로운 컴파일러 경고가 발생했습니다. 이 문제를 방지하기 위해 이 PEP에서는 반환 타입을 변경하지 않습니다.
하위 호환성
이 PEP는 C API와 호환되지 않는 변경을 피하도록 설계되었습니다.
이제 제한 C API 버전 3.11을 명시적으로 대상으로 하는 C 확장만 함수에 예상되는 타입을 전달해야 합니다. 포인터 인자는 더 이상 예상되는 타입으로 캐스팅되지 않습니다.
새로운 컴파일러 경고가 발생하지 않도록 포인터 타입의 함수 인자는 여전히 캐스팅하며 반환 타입은 변경하지 않습니다.
대입문에서 l-value로 사용할 수 있는 매크로는 호환되지 않는 변경을 피하기 위해 이 PEP에서 수정하지 않습니다.
매크로의 함정 예시
부작용의 중복
매크로:
#define PySet_Check(ob) \
(Py_IS_TYPE(ob, &PySet_Type) \
|| PyType_IsSubtype(Py_TYPE(ob), &PySet_Type))
#define Py_IS_NAN(X) ((X) != (X))
op 인자 또는 X 인자에 부작용이 있으면 부작용이 중복됩니다. PySet_Check() 및 Py_IS_NAN()에서 각각 두 번 실행됩니다.
예를 들어 PyUnicode_WRITE(kind, data, pos++, ch) 코드의 pos++ 인자에는 부작용이 있습니다. PyUnicode_WRITE() 매크로가 세 번째 인자를 한 번만 사용하므로 pos++의 부작용이 중복되지 않아 이 코드는 안전합니다.
잘못된 중첩
bpo-43181: Python macros don’t shield arguments의 예시입니다. 그 전에 있던 PyObject_TypeCheck() 매크로는 수정되었습니다.
#define PyObject_TypeCheck(ob, tp) \
(Py_IS_TYPE(ob, tp) || PyType_IsSubtype(Py_TYPE(ob), (tp)))
C++ 사용 예시:
PyObject_TypeCheck(ob, U(f<a,b>(c)))
전처리기는 먼저 이를 확장합니다.
(Py_IS_TYPE(ob, f<a,b>(c)) || ...)
전처리기에서는 C++ "<" 및 ">" 문자를 괄호로 취급하지 않으므로 Py_IS_TYPE() 매크로가 인자 3개와 함께 호출됩니다.
obf<ab>(c)
Py_IS_TYPE()가 2개의 인수만 받기 때문에 컴파일이 오류와 함께 실패합니다.
버그는 PyObject_TypeCheck()의 op및 tp인자를 괄호 안에 넣어야 한다는 것입니다. Py_IS_TYPE(ob, tp)를 Py_IS_TYPE((ob), (tp))로 바꾸십시오. 일반적인 C 코드에서는 이러한 괄호가 불필요하고 버그로 보일 수 있으므로, 매크로를 작성할 때 자주 빠뜨립니다.
매크로 함정을 피하기 위해 PyObject_TypeCheck() 매크로를 정적 인라인 함수로 변환했습니다: commit.
읽기 어려운 매크로의 예
PyObject_INIT()
반환 값을 갖는 매크로에서 쉼표 사용을 보여 주는 예
Python 3.7 매크로:
#define PyObject_INIT(op, typeobj) \
( Py_TYPE(op) = (typeobj), _Py_NewReference((PyObject *)(op)), (op) )
Python 3.8 함수(간소화된 코드):
static inline PyObject*
_PyObject_INIT(PyObject *op, PyTypeObject *typeobj)
{
Py_TYPE(op) = typeobj;
_Py_NewReference(op);
return op;
}
#define PyObject_INIT(op, typeobj) \
_PyObject_INIT(_PyObject_CAST(op), (typeobj))
- 함수에는 줄 연속 문자
"\"가 필요하지 않습니다. - 매크로 끝의 놀라운
", (op)"구문 대신 명시적인"return op;"를 사용합니다. - 하나의 긴 줄로 작성하는 대신 여러 줄에 짧은 문장을 사용합니다.
- 함수 내부에서 op 인자는 잘 정의된
PyObject*타입을 가지므로(PyObject *)(op)와 같은 캐스트가 필요하지 않습니다. - 인수를 괄호 안에 넣을 필요가 없습니다.
(typeobj)대신typeobj를 사용하십시오.
_Py_NewReference()
매크로 내부에서 #ifdef를 사용하는 예
Python 3.7 매크로(간소화된 코드):
#ifdef COUNT_ALLOCS
# define _Py_INC_TPALLOCS(OP) inc_count(Py_TYPE(OP))
# define _Py_COUNT_ALLOCS_COMMA ,
#else
# define _Py_INC_TPALLOCS(OP)
# define _Py_COUNT_ALLOCS_COMMA
#endif /* COUNT_ALLOCS */
#define _Py_NewReference(op) ( \
_Py_INC_TPALLOCS(op) _Py_COUNT_ALLOCS_COMMA \
Py_REFCNT(op) = 1)
Python 3.8 함수(간소화된 코드):
static inline void _Py_NewReference(PyObject *op)
{
_Py_INC_TPALLOCS(op);
Py_REFCNT(op) = 1;
}
PyUnicode_READ_CHAR()
이 매크로는 인수를 재사용하며, PyUnicode_KIND를 여러 번 호출할 수도 있습니다.
#define PyUnicode_READ_CHAR(unicode, index) \
(assert(PyUnicode_Check(unicode)), \
assert(PyUnicode_IS_READY(unicode)), \
(Py_UCS4) \
(PyUnicode_KIND((unicode)) == PyUnicode_1BYTE_KIND ? \
((const Py_UCS1 *)(PyUnicode_DATA((unicode))))[(index)] : \
(PyUnicode_KIND((unicode)) == PyUnicode_2BYTE_KIND ? \
((const Py_UCS2 *)(PyUnicode_DATA((unicode))))[(index)] : \
((const Py_UCS4 *)(PyUnicode_DATA((unicode))))[(index)] \
) \
))
정적 인라인 함수로 가능한 구현:
static inline Py_UCS4
PyUnicode_READ_CHAR(PyObject *unicode, Py_ssize_t index)
{
assert(PyUnicode_Check(unicode));
assert(PyUnicode_IS_READY(unicode));
switch (PyUnicode_KIND(unicode)) {
case PyUnicode_1BYTE_KIND:
return (Py_UCS4)((const Py_UCS1 *)(PyUnicode_DATA(unicode)))[index];
case PyUnicode_2BYTE_KIND:
return (Py_UCS4)((const Py_UCS2 *)(PyUnicode_DATA(unicode)))[index];
case PyUnicode_4BYTE_KIND:
default:
return (Py_UCS4)((const Py_UCS4 *)(PyUnicode_DATA(unicode)))[index];
}
}
Python 3.8부터 함수로 변환된 매크로
다음은 Python 3.8과 Python 3.11 사이에 이미 함수로 변환된 매크로 목록입니다. 변환된 일부 매크로(예: Py_INCREF())가 C 확장에서 매우 흔히 사용되지만, 이러한 변환은 Python 성능에 큰 영향을 주지 않았으며 대부분 하위 호환성을 깨뜨리지 않았습니다.
정적 인라인 함수로 변환된 매크로
Python 3.8:
Py_DECREF()Py_INCREF()Py_XDECREF()Py_XINCREF()PyObject_INIT()PyObject_INIT_VAR()_PyObject_GC_UNTRACK()_Py_Dealloc()
일반 함수로 변환된 매크로
Python 3.9:
PyIndex_Check()PyObject_CheckBuffer()PyObject_GET_WEAKREFS_LISTPTR()PyObject_IS_GC()PyObject_NEW():PyObject_New()의 별칭입니다.PyObject_NEW_VAR():PyObjectVar_New()의 별칭입니다.
LTO 없이 빌드된 Python에서 성능 저하를 방지하기 위해 내부 C API에 비공개 정적 인라인 함수가 추가되었습니다.
_PyIndex_Check()_PyObject_IS_GC()_PyType_HasFeature()_PyType_IS_GC()
정적 인라인 함수가 일반 함수로 변환되었습니다.
Python 3.11:
PyObject_CallOneArg()PyObject_Vectorcall()PyVectorcall_Function()_PyObject_FastCall()
LTO 없이 빌드된 Python에서 성능 저하를 방지하기 위해 내부 C API에 비공개 정적 인라인 함수가 추가되었습니다.
_PyVectorcall_FunctionInline()
호환성이 깨지는 변경 사항
변환된 다른 매크로는 하위 호환성을 깨뜨리지 않았지만, 예외가 있습니다.
Py_REFCNT(), Py_TYPE() 및 Py_SIZE()의 3개 매크로는 대입문에서 l-value로 사용하지 못하도록 Python 3.10 및 3.11에서 정적 인라인 함수로 변환되었습니다. 이는 의도적으로 적용된 호환성이 깨지는 변경 사항입니다. 그 근거는 bpo-39573에서 확인하십시오.
이 PEP에서는 새로운 호환성이 깨지는 변경 사항이 도입되는 것을 방지하기 위해 l-value로 사용할 수 있는 매크로를 변환하는 것을 제안하지 않습니다.
성능 관련 우려와 벤치마크
매크로를 함수로 변환하면 성능이 저하될 수 있다는 우려가 있었습니다.
이 절에서는 다음 정적 인라인 함수를 매크로로 대체하는 PR 29728을 사용하여 성능 관련 우려를 설명하고 벤치마크 결과를 보여 줍니다:
PyObject_TypeCheck()PyType_Check(),PyType_CheckExact()PyType_HasFeature()PyVectorcall_NARGS()Py_DECREF(),Py_XDECREF()Py_INCREF(),Py_XINCREF()Py_IS_TYPE()Py_NewRef()Py_REFCNT(),Py_TYPE(),Py_SIZE()
벤치마크는 논리 CPU 8개(물리 CPU 코어 4개)가 장착된 노트북에서 GCC 11을 사용하여 Fedora 35(Linux)에서 실행했습니다.
정적 인라인 함수
우선, 매크로를 정적 인라인 함수로 변환해도 성능에는 무시할 수 있을 정도의 영향만 있습니다. 측정된 차이는 관련 없는 요인으로 인한 잡음과 일치합니다.
정적 인라인 함수는 C99 표준의 새로운 기능입니다. 최신 C 컴파일러에는 함수를 인라인할지 여부를 결정하는 효율적인 휴리스틱이 있습니다.
C 컴파일러가 인라인하지 않기로 결정했다면, 그럴 만한 이유가 있을 가능성이 높습니다. 예를 들어, 인라인하면 스택에서 레지스터 값을 저장하고 복원해야 하는 레지스터를 재사용하게 되어 스택 메모리 사용량이 증가하거나, 효율성이 떨어질 수 있습니다.
gcc -O3 및 LTO와 PGO를 사용하여 릴리스 모드로 빌드한 Python에서 ./python -m test -j5 명령의 벤치마크입니다.
- 매크로(PR 29728): 361초 +- 1초
- 정적 인라인 함수(참조): 361초 +- 1초
정적 인라인 함수가 인라인되는 경우, 매크로와 정적 인라인 함수 사이에는 유의미한 성능 차이가 없습니다.
디버그 빌드
디버그 빌드에서는 매크로를 함수로 변환하면 성능이 저하될 수 있습니다. 이는 디버깅 가능성이 향상되는 것으로 보완됩니다. 디버거가 함수 이름을 검색하고 함수 내부에 중단점을 설정할 수 있습니다.
Windows에서 Visual Studio로 Python을 디버그 모드로 빌드하면 정적 인라인 함수가 인라인되지 않습니다.
다른 플랫폼에서는 ./configure --with-pydebug가 이를 지원하는 컴파일러(GCC와 LLVM Clang 포함)에서 -Og 컴파일러 옵션을 사용합니다. -Og은 “디버깅 환경을 최적화한다”는 의미입니다. 그렇지 않으면 -O0 컴파일러 옵션을 사용합니다. -O0은 “대부분의 최적화를 비활성화한다”는 의미입니다.
GCC 11에서는 gcc -Og가 정적 인라인 함수를 인라인할 수 있지만, gcc -O0은 정적 인라인 함수를 인라인하지 않습니다.
gcc -O0을 사용하여 디버그 모드로 빌드한 Python에서 ./python -m test -j10 명령의 벤치마크입니다(즉, 인라인을 포함한 컴파일러 최적화가 명시적으로 비활성화됩니다).
- 매크로(PR 29728): 345초 ± 5초
- 정적 인라인 함수(참조): 360초 ± 6초
컴파일러가 정적 인라인 함수를 인라인하지 않으면, 매크로를 정적 인라인 함수로 대체할 때 Python이 1.04배 더 느려집니다.
벤치마크는 Python 디버그 빌드에서 실행해서는 안 된다는 점에 유의하십시오. 또한 최상의 성능과 신뢰할 수 있는 벤치마크를 위해 링크 타임 최적화(LTO)와 프로파일 기반 최적화(PGO)를 사용하는 것이 권장됩니다. PGO는 함수의 인라인화 여부를 컴파일러가 결정하는 데 도움을 줍니다.
강제 인라인화
Py_ALWAYS_INLINE 매크로를 사용하여 인라인화를 강제할 수 있습니다. 이 매크로는 GCC 및 Clang에서는 __attribute__((always_inline))를, MSC에서는 __forceinline를 사용합니다.
Py_ALWAYS_INLINE을 사용하려는 이전 시도에서는 아무런 이점이 나타나지 않아 중단되었습니다. 예를 들어 bpo-45094 “디버그 빌드에서 정적 인라인 함수(Py_INCREF, Py_TYPE)에 __forceinline 및 __attribute__((always_inline))을 사용하는 것을 고려하십시오”를 참조하십시오.
2018년에 Py_INCREF() 매크로를 정적 인라인 함수로 변환할 때 (commit), 인라인화를 강제하지 않기로 결정했습니다. 기계어 코드를 여러 C 컴파일러와 컴파일러 옵션으로 분석했으며, Py_INCREF()은 인라인화를 강제하지 않아도 항상 인라인화되었습니다. 인라인화되지 않은 유일한 경우는 디버그 빌드였습니다. bpo-35059 “Py_INCREF() 및 PyObject_INIT()을 인라인 함수로 변환”에서 논의를 참조하십시오.
인라인화 비활성화
반면 Py_NO_INLINE 매크로를 사용하여 인라인화를 비활성화할 수 있습니다. 이 매크로는 스택 메모리 사용량을 줄이거나, 일반적으로 코드를 더 적극적으로 인라인화하는 LTO+PGO 빌드에서 인라인화를 방지하는 데 사용할 수 있습니다. bpo-33720을 참조하십시오. Py_NO_INLINE 매크로는 GCC 및 Clang에서는 __attribute__ ((noinline))을, MSC에서는 __declspec(noinline)을 사용합니다.
이 기법을 사용할 수는 있지만, 현재로서는 이것이 유용할 구체적인 함수를 알지 못합니다. 매크로를 사용하면 인라인화를 전혀 비활성화할 수 없다는 점에 유의하십시오.
거부된 아이디어
매크로를 유지하되 일부 매크로 문제 수정
매크로는 모든 C 컴파일러에서 항상 “인라인화”됩니다.
부작용 중복 문제는 매크로를 호출하는 쪽에서 우회할 수 있습니다.
매크로를 사용하는 사람은 “동의한 성인”으로 간주해야 합니다. 매크로를 안전하지 않다고 느끼는 사람은 단순히 매크로를 사용하지 않아야 합니다.
매크로는 본질적으로 오류가 발생하기 쉽고, 매크로 코드를 작성하고 검토할 때 매크로의 함정을 놓치기 너무 쉽기 때문에 이러한 아이디어는 거부되었습니다. 또한 매크로는 함수보다 읽고 유지 관리하기가 더 어렵습니다.
이후 기록
python-dev 메일링 리스트 스레드:
참고 자료
- bpo-45490: [C API] PEP 670: Python C API의 매크로를 함수로 변환합니다(2021년 10월).
- 안전하지 않은 매크로를 어떻게 처리할 것인가(2021년 3월).
- bpo-43502: [C-API] 명백히 안전하지 않은 매크로를 정적 인라인 함수로 변환합니다(2021년 3월).
버전 기록입니다.
- 버전 2입니다.
- 인자 유형과 반환 유형을 변경하지 않는 것에 대한 더 엄격한 정책입니다.
- 새로운 컴파일러 경고가 발생하지 않도록 포인터 인자에 캐스트가 필요한 이유를 더 잘 설명합니다.
- l-value로 사용할 수 있는 매크로는 이제 PEP에서 수정하지 않습니다.
- 여러 반환 유형을 갖는 매크로는 이제 PEP에서 수정하지 않습니다.
- Limited C API 버전 3.11에서는 더 이상 포인터 인자를 캐스트하지 않습니다.
- “반환 값을 가져서는 안 되는” 매크로의 반환 값을 더 이상 제거하지 않습니다.
- “Python 3.8 이후 함수로 변환된 매크로” 절을 추가합니다.
- “매크로와 정적 인라인 함수를 비교하는 벤치마크” 절을 추가합니다.
- 버전 1: 최초 공개 버전입니다.
Copyright
This document is placed in the public domain or under the CC0-1.0-Universal license, whichever is more permissive.