PEP 670 – 将 Python C API 中的宏转换为函数
- 作者:
- Erlend Egeberg Aasland <erlend at python.org>, Victor Stinner <vstinner at python.org>
- 状态:
- 最终版
- 类型:
- 标准跟踪
- 创建日期:
- 2021年10月19日
- Python 版本:
- 3.11
- 发布历史:
- 2021年10月20日, 2022年2月8日, 2022年2月22日
- 决议:
- Python-Dev 帖子
摘要
C API 中的宏将被转换为静态内联函数或常规函数。这将有助于避免 C/C++ 中的宏陷阱,并使这些函数可以从其他编程语言中使用。
为避免编译器警告,指针类型的函数参数将使用额外的宏强制转换为适当的类型。在受限 C API 3.11 版中不会进行强制转换:选择使用新受限 API 的用户可能需要添加强制转换为精确的预期类型。
为避免引入不兼容的更改,不能用作赋值左值的宏将不会被转换。
基本原理
即使是经验丰富的 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(从调用函数返回)。 - 可以用作赋值左值的宏。这将是一个不兼容的更改,超出本 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*
:如果使用 const void*
变量调用函数,移除强制转换为 void*
将发出新警告。例如,PyUnicode_WRITE()
宏将其 data 参数强制转换为 void*
,因此它目前接受 const void*
类型,即使它写入 data。本 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 扩展现在必须将预期类型传递给函数:指针参数不再强制转换为预期类型。
指针类型的函数参数仍然强制转换,并且返回类型不变,以防止发出新的编译器警告。
本 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()
宏只使用其第 3 个参数一次,因此不会重复 pos++
的副作用。
错误嵌套
示例 bpo-43181:Python 宏不保护参数。修复前的 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)
编译失败,Py_IS_TYPE()
只有一个参数。
bug 是 PyObject_TypeCheck()
的 *op* 和 *tp* 参数必须用括号括起来:将 Py_IS_TYPE(ob, tp)
替换为 Py_IS_TYPE((ob), (tp))
。在常规 C 代码中,这些括号是多余的,可能被视为一个 bug,因此在编写宏时经常被遗忘。
为避免宏陷阱,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))
- 该函数不需要续行符
"\"
。 - 它有一个显式的
"return op;"
,而不是宏末尾令人惊讶的", (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()
不兼容的更改
虽然其他转换的宏没有破坏向后兼容性,但有一个例外。
在 Python 3.10 和 3.11 中,Py_REFCNT()
、Py_TYPE()
和 Py_SIZE()
这三个宏已转换为静态内联函数,以禁止将其用作赋值左值。这是一个故意造成的不兼容更改:请参阅 bpo-39573 以了解其原理。
本 PEP 不建议转换可用作左值的宏,以避免引入新的不兼容更改。
性能问题和基准测试
有人担心将宏转换为函数会降低性能。
本节解释了性能问题并显示了使用 PR 29728 的基准测试结果,该 PR 将以下静态内联函数替换为宏
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()
基准测试在 Fedora 35 (Linux) 上使用 GCC 11 在一台具有 8 个逻辑 CPU (4 个物理 CPU 核) 的笔记本电脑上运行。
静态内联函数
首先,将宏转换为 静态内联 函数对性能的影响可以忽略不计:测量到的差异与由于无关因素造成的噪声一致。
静态内联函数是 C99 标准中的一个新特性。现代 C 编译器具有高效的启发式算法来决定函数是否应该内联。
当 C 编译器决定不内联时,很可能有充分的理由。例如,内联会重用一个寄存器,这需要将寄存器值保存/恢复到堆栈中,从而增加堆栈内存使用,或者效率较低。
在发布模式下使用 gcc -O3
、LTO 和 PGO 构建的 Python 上,./python -m test -j5
命令的基准测试
- 宏 (PR 29728): 361 秒 ± 1 秒
- 静态内联函数 (参考): 361 秒 ± 1 秒
当静态内联函数 被内联时,宏和静态内联函数之间 没有显著的性能差异。
调试构建
在调试构建中,将宏转换为函数 *可能* 会导致性能下降。这可以通过更好的可调试性来弥补:调试器可以检索函数名称,在函数内部设置断点等。
在 Windows 上,当 Python 由 Visual Studio 在调试模式下构建时,静态内联函数不会被内联。
在其他平台上,./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))
”。
当 Py_INCREF()
宏在 2018 年转换为静态内联函数时 (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 邮件列表线程
- PEP 670 第 2 版 - 将 Python C API 中的宏转换为函数(2022 年 2 月)
- 指导委员会对 PEP 670 的回复 – 将 Python C API 中的宏转换为函数(2022 年 2 月)
- PEP 670:将 Python C API 中的宏转换为函数(2021 年 10 月)
参考资料
版本历史
- 版本 2
- 更严格的政策,不改变参数类型和返回类型。
- 更好地解释为什么指针参数需要强制转换才能不发出新的编译器警告。
- 可作为左值使用的宏不再由 PEP 修改。
- 具有多个返回类型的宏不再由 PEP 修改。
- 受限 C API 3.11 版不再强制转换指针参数。
- 不再删除“不应有返回值”的宏的返回值。
- 添加“自 Python 3.8 以来转换为函数的宏”部分。
- 添加“宏和静态内联函数比较基准测试”部分。
- 版本 1:第一个公开版本
版权
本文档置于公共领域或 CC0-1.0-Universal 许可证下,以更宽松者为准。
来源:https://github.com/python/peps/blob/main/peps/pep-0670.rst