GeezerVisonThe World Wide Web, restored to 800 pixels of glory. |
| Home|About|Gaming cabinet|Archives|How it works|Starter sites |
GeezerVison is showing peps.python.org — PEP 670 – Convert macros to functions in the Python C API | peps.python.org
PEP 670 – Convert macros to functions in the Python C API
PEP 670 – Convert macros to functions in the Python C API
AbstractMacros in the C API will be converted to static inline functions or regular functions. This will help avoid macro pitfalls in C/C++, and make the functions usable from other programming languages. To avoid compiler warnings, function arguments of pointer types will be cast to appropriate types using additional macros. The cast will not be done in the limited C API version 3.11: users who opt in to the new limited API may need to add casts to the exact expected type. To avoid introducing incompatible changes, macros which can be used as l-value in an assignment will not be converted. RationaleThe use of macros may have unintended adverse effects that are hard to avoid, even for experienced C developers. Some issues have been known for years, while others have been discovered recently in Python. Working around macro pitfalls makes the macro code harder to read and to maintain. Converting macros to functions has multiple advantages:
Converting macros and static inline functions to regular functions makes these regular functions accessible to projects which use Python but cannot use macros and static inline functions. SpecificationConvert macros to static inline functionsMost macros will be converted to static inline functions. The following macros will not be converted:
Convert static inline functions to regular functionsStatic inline functions in the public C API may be converted to regular functions, but only if there is no measurable performance impact of changing the function. The performance impact should be measured with benchmarks. Cast pointer argumentsCurrently, most macros accepting pointers cast pointer arguments to
their expected types. For example, in Python 3.6, the #define Py_TYPE(ob) (((PyObject*)(ob))->ob_type)
The Functions are strongly typed, and can only accept one type of argument. To avoid compiler errors and warnings in existing code, when a macro is converted to a function and the macro casts at least one of its arguments a new macro will be added to keep the cast. The new macro and the function will have the same name. Example with the static inline PyTypeObject* Py_TYPE(PyObject *ob) { return ob->ob_type; } #define Py_TYPE(ob) Py_TYPE((PyObject*)(ob)) The cast is kept for all pointer types, not only Avoid the cast in the limited C API version 3.11The casts will be excluded from the limited C API version 3.11 and newer. When an API user opts into the new limited API, they must pass the expected type or perform the cast. As an example, 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 Return type is not changedWhen a macro is converted to a function, its return type must not change to prevent emitting new compiler warnings. For example, Python 3.7 changed the return type of Backwards CompatibilityThe PEP is designed to avoid C API incompatible changes. Only C extensions explicitly targeting the limited C API version 3.11 must now pass the expected types to functions: pointer arguments are no longer cast to the expected types. Function arguments of pointer types are still cast and return types are not changed to prevent emitting new compiler warnings. Macros which can be used as l-value in an assignment are not modified by this PEP to avoid incompatible changes. Examples of Macro PitfallsDuplication of side effectsMacros: #define PySet_Check(ob) \ (Py_IS_TYPE(ob, &PySet_Type) \ || PyType_IsSubtype(Py_TYPE(ob), &PySet_Type)) #define Py_IS_NAN(X) ((X) != (X)) If the op or the X argument has a side effect, the side effect is
duplicated: it executed twice by For example, the MisnestingExample of the bpo-43181: Python macros don’t shield arguments. The #define PyObject_TypeCheck(ob, tp) \ (Py_IS_TYPE(ob, tp) || PyType_IsSubtype(Py_TYPE(ob), (tp))) C++ usage example: PyObject_TypeCheck(ob, U(f<a,b>(c))) The preprocessor first expands it: (Py_IS_TYPE(ob, f<a,b>(c)) || ...) C++
The compilation fails with an error on The bug is that the op and tp arguments of To avoid Macro Pitfalls, the Examples of hard to read macrosPyObject_INIT()Example showing the usage of commas in a macro which has a return value. Python 3.7 macro: #define PyObject_INIT(op, typeobj) \ ( Py_TYPE(op) = (typeobj), _Py_NewReference((PyObject *)(op)), (op) ) Python 3.8 function (simplified code): 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))
_Py_NewReference()Example showing the usage of an Python 3.7 macro (simplified code): #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 function (simplified code): static inline void _Py_NewReference(PyObject *op) { _Py_INC_TPALLOCS(op); Py_REFCNT(op) = 1; } PyUnicode_READ_CHAR()This macro reuses arguments, and possibly calls #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)] \ ) \ )) Possible implementation as a static inlined function: 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]; } } Macros converted to functions since Python 3.8This is a list of macros already converted to functions between
Python 3.8 and Python 3.11.
Even though some converted macros (like Macros converted to static inline functionsPython 3.8:
Macros converted to regular functionsPython 3.9:
To avoid performance slowdown on Python built without LTO, private static inline functions have been added to the internal C API:
Static inline functions converted to regular functionsPython 3.11:
To avoid performance slowdown on Python built without LTO, a private static inline function has been added to the internal C API:
Incompatible changesWhile other converted macros didn’t break the backward compatibility, there is an exception. The 3 macros This PEP does not propose converting macros which can be used as l-value to avoid introducing new incompatible changes. Performance concerns and benchmarksThere have been concerns that converting macros to functions can degrade performance. This section explains performance concerns and shows benchmark results using PR 29728, which replaces the following static inline functions with macros:
The benchmarks were run on Fedora 35 (Linux) with GCC 11 on a laptop with 8 logical CPUs (4 physical CPU cores). Static inline functionsFirst of all, converting macros to static inline functions has negligible impact on performance: the measured differences are consistent with noise due to unrelated factors. Static inline functions are a new feature in the C99 standard. Modern C compilers have efficient heuristics to decide if a function should be inlined or not. When a C compiler decides to not inline, there is likely a good reason. For example, inlining would reuse a register which requires to save/restore the register value on the stack and so increases the stack memory usage, or be less efficient. Benchmark of the
There is no significant performance difference between macros and static inline functions when static inline functions are inlined. Debug buildPerformance in debug builds can suffer when macros are converted to functions. This is compensated by better debuggability: debuggers can retrieve function names, set breakpoints inside functions, etc. On Windows, when Python is built in debug mode by Visual Studio, static inline functions are not inlined. On other platforms, With GCC 11, Benchmark of the
Replacing macros with static inline functions makes Python 1.04x slower when the compiler does not inline static inline functions. Note that benchmarks should not be run on a Python debug build. Moreover, using link-time optimization (LTO) and profile-guided optimization (PGO) is recommended for best performance and reliable benchmarks. PGO helps the compiler to decide if functions should be inlined or not. Force inliningThe Previous attempts to use When the Disabling inliningOn the other side, the This technique is available, though we currently don’t know a concrete function for which it would be useful. Note that with macros, it is not possible to disable inlining at all. Rejected IdeasKeep macros, but fix some macro issuesMacros are always “inlined” with any C compiler. The duplication of side effects can be worked around in the caller of the macro. People using macros should be considered “consenting adults”. People who feel unsafe with macros should simply not use them. These ideas are rejected because macros are error prone, and it is too easy to miss a macro pitfall when writing and reviewing macro code. Moreover, macros are harder to read and maintain than functions. Post Historypython-dev mailing list threads: References
Version History
CopyrightThis document is placed in the public domain or under the CC0-1.0-Universal license, whichever is more permissive. |
| You are visitor 01402503 — GeezerVison does not store the pages you look up. Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required. |