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

Python 개선 제안 한국어 번역

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개의 인자로 호출됩니다:

  • ob
  • f<a
  • b>(c)

인자를 2개만 받는 Py_IS_TYPE()에서 오류가 발생하여 컴파일이 실패합니다.

버그의 원인은 PyObject_TypeCheck()의 op와 tp 인자를 괄호로 감싸야 한다는 점입니다: Py_IS_TYPE(ob, tp)를 Py_IS_TYPE((ob), (tp))로 바꾸십시오. 일반적인 C 코드에서 이러한 괄호는 불필요하고 버그로 보일 수도 있기 때문에, 매크로를 작성할 때 자주 빠뜨리게 됩니다.

매크로 함정을 피하기 위해 PyObject_TypeCheck() 매크로는 정적 인라인 함수로 변환되었습니다: 커밋.

읽기 어려운 매크로의 예

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개)를 갖춘 노트북에서 Fedora 35(Linux)와 GCC 11로 실행했습니다.

static inline 함수

우선, 매크로를 static inline 함수로 변환해도 성능에 미치는 영향은 무시할 만한 수준입니다. 측정된 차이는 무관한 요인에서 비롯된 노이즈로 설명되는 정도입니다.

static inline 함수는 C99 표준에서 새로 도입된 기능입니다. 최신 C 컴파일러는 함수를 인라인할지 여부를 결정하는 효율적인 휴리스틱을 갖추고 있습니다.

C 컴파일러가 인라인하지 않기로 결정했다면 그럴 만한 이유가 있을 가능성이 높습니다. 예를 들어, 인라인하면 레지스터를 재사용하게 되어 레지스터 값을 스택에 저장하고 복원해야 하므로 스택 메모리 사용량이 늘어나거나, 효율이 떨어질 수 있습니다.

gcc -O3, LTO, PGO를 사용하여 릴리스 모드로 빌드한 Python에서 ./python -m test -j5 명령을 실행한 벤치마크 결과:

  • 매크로(PR 29728): 361초 +- 1초
  • static inline 함수(기준): 361초 +- 1초

static inline 함수가 인라인될 때는 매크로와 static inline 함수 사이에 유의미한 성능 차이가 없습니다.

디버그 빌드

매크로를 함수로 변환하면 디버그 빌드에서는 성능이 저하될 수 있습니다. 대신 디버깅이 쉬워지므로 이를 상쇄합니다. 디버거가 함수 이름을 조회하고, 함수 내부에 중단점을 설정하는 등의 작업을 할 수 있습니다.

Windows에서 Visual Studio로 Python을 디버그 모드로 빌드하면 static inline 함수가 인라인되지 않습니다.

다른 플랫폼에서는 ./configure --with-pydebug가 이를 지원하는 컴파일러(GCC와 LLVM Clang 포함)에서 -Og 컴파일러 옵션을 사용합니다. -Og는 “디버깅 경험 최적화”를 의미합니다. 그렇지 않으면 -O0 컴파일러 옵션이 사용됩니다. -O0은 “대부분의 최적화 비활성화”를 의미합니다.

GCC 11에서 gcc -Og는 static inline 함수를 인라인할 수 있지만, gcc -O0은 static inline 함수를 인라인하지 않습니다.

gcc -O0으로(즉, 인라인을 포함한 컴파일러 최적화를 명시적으로 비활성화하여) 디버그 모드로 빌드한 Python에서 ./python -m test -j10 명령을 실행한 벤치마크 결과:

  • 매크로(PR 29728): 345초 ± 5초
  • static inline 함수(기준): 360초 ± 6초

컴파일러가 static inline 함수를 인라인하지 않을 때는 매크로를 static inline 함수로 대체하면 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() 매크로를 정적 인라인 함수로 변환했을 때(커밋), 인라인을 강제하지 않기로 결정했습니다. 여러 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 메일링 리스트 스레드:

참고 문헌

버전 이력

  • 버전 2:
    • 인자 타입과 반환 타입을 변경하지 않는다는 정책을 더 엄격하게 적용합니다.
    • 새로운 컴파일러 경고가 발생하지 않도록 포인터 인자에 캐스트가 필요한 이유를 더 잘 설명합니다.
    • l-value로 사용할 수 있는 매크로는 더 이상 이 PEP에서 수정하지 않습니다.
    • 여러 반환 타입을 가지는 매크로는 더 이상 이 PEP에서 수정하지 않습니다.
    • 제한된 C API 버전 3.11에서는 더 이상 포인터 인자를 캐스트하지 않습니다.
    • “반환값이 없어야 하는” 매크로의 반환값을 더 이상 제거하지 않습니다.
    • “Python 3.8 이후 함수로 변환된 매크로” 절을 추가합니다.
    • “매크로와 정적 인라인 함수를 비교하는 벤치마크” 절을 추가합니다.
  • 버전 1: 최초 공개 버전