Following system colour scheme - Python 增强提案 Selected dark colour scheme - Python 增强提案 Selected light colour scheme - Python 增强提案

Python 增强提案

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_INLINEPy_MEMCPY()
  • 用于定义而不是行为的宏。例如:PyAPI_FUNCPy_DEPRECATEDPy_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*提交)。当构建期望 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))

如果 opX 参数具有副作用,则副作用会重复:它被 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)

编译失败,并在仅接受 2 个参数的 Py_IS_TYPE() 上出现错误。

错误在于 PyObject_TypeCheck()optp 参数必须放在括号内:将 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))
  • 该函数不需要行连接符 "\"
  • 它使用显式的 "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()

不兼容的更改

虽然其他转换的宏没有破坏向后兼容性,但有一个例外。

3 个宏 Py_REFCNT()Py_TYPE()Py_SIZE() 已在 Python 3.10 和 3.11 中转换为静态内联函数,以禁止将其用作赋值中的左值。这是一个有意做出的不兼容更改:请参阅 bpo-39573 以了解其原因。

本 PEP 并不建议转换可以用作左值的宏,以避免引入新的不兼容更改。

性能问题和基准测试

有人担心将宏转换为函数可能会降低性能。

本节解释了性能问题,并展示了使用 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()

基准测试是在 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_INCREFPy_TYPE)上使用 __forceinline__attribute__((always_inline)) 进行调试版本构建”。

Py_INCREF() 宏在 2018 年转换为静态内联函数时(提交),决定不强制内联。使用多个 C 编译器和编译器选项分析了机器代码,并且 Py_INCREF() 始终被内联,无需强制内联。唯一未内联的情况是调试版本构建。请参阅 bpo-35059“将 Py_INCREF()PyObject_INIT() 转换为内联函数”中的讨论。

禁用内联

另一方面,Py_NO_INLINE 宏可用于禁用内联。它可用于减少堆栈内存使用量,或防止在 LTO+PGO 构建上进行内联,这些构建通常更积极地内联代码:请参阅 bpo-33720Py_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
    • 关于不更改参数类型和返回类型的更严格的策略。
    • 更好地解释为什么指针参数需要强制类型转换才能不发出新的编译器警告。
    • PEP 不再修改可以用作左值的宏。
    • PEP 不再修改具有多个返回类型的宏。
    • 受限的 C API 版本 3.11 不再强制转换指针参数。
    • 不再删除“不应具有返回值”的宏的返回值。
    • 添加“自 Python 3.8 以来已转换为函数的宏”部分。
    • 添加“比较宏和静态内联函数的基准测试”部分。
  • 版本 1:第一个公开版本

来源:https://github.com/python/peps/blob/main/peps/pep-0670.rst

上次修改时间:2023-10-04 23:18:07 GMT