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

Python 增强提案

PEP 757 – 导入-导出 Python 整数的 C API

作者:
Sergey B Kirpichev <skirpichev at gmail.com>, Victor Stinner <vstinner at python.org>
讨论至:
Discourse 帖子
状态:
最终版
类型:
标准跟踪
创建日期:
2024年9月13日
Python 版本:
3.14
发布历史:
2024年9月14日
决议:
2024年12月8日

目录

重要

本 PEP 是一份历史文档。最新的规范文档现在可以在 导出 APIPyLongWriter API 中找到。

×

有关如何提出更改,请参阅 PEP 1

摘要

添加一个新的 C API,用于导入和导出 Python 整数,即 int 对象:特别是 PyLongWriter_Create()PyLong_Export() 函数。

基本原理

gmpy2SAGEPython-FLINT 等项目直接访问 Python“内部”(PyLongObject 结构)或使用低效的临时格式(Python-FLINT 的十六进制字符串)来导入和导出 Python int 对象。Python int 的实现在 Python 3.12 中发生了变化,增加了标签和“紧凑值”。

在 3.13 alpha 1 版本中,私有的未公开的 _PyLong_New() 函数已被移除,但这些项目正在使用它来导入 Python 整数。该私有函数已在 3.13 alpha 2 中恢复。

需要一个公共且高效的抽象层,以便 Python 与这些项目交互,而无需暴露实现细节。这将允许 Python 在不破坏这些项目的情况下更改其内部结构。例如,gmpy2 的实现在 CPython 3.9 和 CPython 3.12 中最近进行了更改。

规范

布局 API

GMP 类似 导入-导出 函数所需的数据。

struct PyLongLayout
“数字”(GMP 术语中的“limb”)数组的布局,用于表示任意精度整数的绝对值。

使用 PyLong_GetNativeLayout() 获取 Python int 对象的原生布局,内部用于具有“足够大”绝对值的整数。

另请参阅 sys.int_info,它向 Python 暴露类似的信息。

uint8_t bits_per_digit
每个数字的位数。例如,一个 15 位数字意味着位 0-14 包含有意义的信息。
uint8_t digit_size
数字的字节大小。例如,一个 15 位数字至少需要 2 个字节。
int8_t digits_order
数字顺序
  • 最高有效数字优先为 1
  • 最低有效数字优先为 -1
int8_t digit_endianness
数字字节序
  • 最高有效字节优先(大端)为 1
  • 最低有效字节优先(小端)为 -1
const PyLongLayout *PyLong_GetNativeLayout(void)
获取 Python int 对象的原生布局。

请参阅 PyLongLayout 结构。

此函数不得在 Python 初始化之前或 Python 终结之后调用。返回的布局在 Python 终结之前有效。所有 Python 子解释器都使用相同的布局,因此可以缓存。

导出 API

struct PyLongExport
Python int 对象的导出。

有两种情况

int64_t value
导出 int 对象的原生整数值。仅当 digitsNULL 时有效。
uint8_t negative
如果数字为负,则为 1,否则为 0。仅当 digits 不为 NULL 时有效。
Py_ssize_t ndigits
digits 数组中的数字数量。仅当 digits 不为 NULL 时有效。
const void *digits
无符号数字的只读数组。可以是 NULL

如果 PyLongExport.digits 不为 NULL,则 PyLongExport 结构的私有字段会存储对 Python int 对象的强引用,以确保该结构在调用 PyLong_FreeExport() 之前保持有效。

int PyLong_Export(PyObject *obj, PyLongExport *export_long)
导出 Python int 对象。

export_long 必须指向调用者分配的 PyLongExport 结构。它不能为 NULL

成功时,填充 *export_long 并返回 0。错误时,设置异常并返回 -1。

当不再需要导出时,必须调用 PyLong_FreeExport()

CPython 实现细节:如果 obj 是 Python int 对象或其子类,此函数始终成功。

在 CPython 3.14 中,PyLong_Export() 中不需要内存复制,它只是一个暴露 Python int 内部数字数组的简单包装器。

void PyLong_FreeExport(PyLongExport *export_long)
释放由 PyLong_Export() 创建的导出 export_long

CPython 实现细节:如果 export_long->digitsNULL,则调用 PyLong_FreeExport() 是可选的。

导入 API

PyLongWriter API 可用于导入整数。

struct PyLongWriter
一个 Python int 写入器实例。

该实例必须通过 PyLongWriter_Finish()PyLongWriter_Discard() 销毁。

PyLongWriter *PyLongWriter_Create(int negative, Py_ssize_t ndigits, void **digits)
创建一个 PyLongWriter

成功时,分配 *digits 并返回一个写入器。错误时,设置异常并返回 NULL

如果数字为负,则 negative1,否则为 0

ndigitsdigits 数组中的数字数量。它必须大于 0。

digits 不能为 NULL。

成功调用此函数后,调用者应填充数字数组 digits,然后调用 PyLongWriter_Finish() 以获取 Python intdigits 的布局由 PyLong_GetNativeLayout() 描述。

数字必须在 [0; (1 << bits_per_digit) - 1] 范围内(其中 bits_per_digit 是每个数字的位数)。任何未使用的最高有效数字必须设置为 0

或者,调用 PyLongWriter_Discard() 以销毁写入器实例,而不创建 int 对象。

在 CPython 3.14 中,PyLongWriter_Create() 的实现是私有函数 _PyLong_New() 的简单包装器。

PyObject *PyLongWriter_Finish(PyLongWriter *writer)
完成由 PyLongWriter_Create() 创建的 PyLongWriter

成功时,返回一个 Python int 对象。错误时,设置异常并返回 NULL

该函数负责规范化数字,并在需要时将对象转换为紧凑整数。

调用后,写入器实例和 digits 数组将失效。

void PyLongWriter_Discard(PyLongWriter *writer)
丢弃由 PyLongWriter_Create() 创建的 PyLongWriter

writer 不能为 NULL

调用后,写入器实例和 digits 数组将失效。

优化小整数导入

提议的导入 API 对大整数高效。与直接访问 Python 内部结构相比,提议的导入 API 在小整数上可能会有显著的性能开销。

对于几个数字的小整数(例如,1 或 2 个数字),可以使用现有 API

实施

基准测试

代码

/* Query parameters of Python’s internal representation of integers. */
const PyLongLayout *layout = PyLong_GetNativeLayout();

size_t int_digit_size = layout->digit_size;
int int_digits_order = layout->digits_order;
size_t int_bits_per_digit = layout->bits_per_digit;
size_t int_nails = int_digit_size*8 - int_bits_per_digit;
int int_endianness = layout->digit_endianness;

导出:PyLong_Export() 与 gmpy2

代码

static int
mpz_set_PyLong(mpz_t z, PyObject *obj)
{
    static PyLongExport long_export;

    if (PyLong_Export(obj, &long_export) < 0) {
        return -1;
    }

    if (long_export.digits) {
        mpz_import(z, long_export.ndigits, int_digits_order, int_digit_size,
                   int_endianness, int_nails, long_export.digits);
        if (long_export.negative) {
            mpz_neg(z, z);
        }
        PyLong_FreeExport(&long_export);
    }
    else {
        const int64_t value = long_export.value;

        if (LONG_MIN <= value && value <= LONG_MAX) {
            mpz_set_si(z, value);
        }
        else {
            mpz_import(z, 1, -1, sizeof(int64_t), 0, 0, &value);
            if (value < 0) {
                mpz_t tmp;
                mpz_init(tmp);
                mpz_ui_pow_ui(tmp, 2, 64);
                mpz_sub(z, z, tmp);
                mpz_clear(tmp);
            }
        }
    }
    return 0;
}

参考代码:gmpy2 master 中 commit 9177648 的 mpz_set_PyLong()

基准测试

import pyperf
from gmpy2 import mpz

runner = pyperf.Runner()
runner.bench_func('1<<7', mpz, 1 << 7)
runner.bench_func('1<<38', mpz, 1 << 38)
runner.bench_func('1<<300', mpz, 1 << 300)
runner.bench_func('1<<3000', mpz, 1 << 3000)

在 Linux Fedora 40 上,CPU 隔离,Python 在发布模式下构建的结果

基准测试 参考 pep757
1<<7 91.3 纳秒 89.9 纳秒:快 1.02 倍
1<<38 120 纳秒 94.9 纳秒:快 1.27 倍
1<<300 196 纳秒 203 纳秒:慢 1.04 倍
1<<3000 939 纳秒 945 纳秒:慢 1.01 倍
几何平均值 (参考) 快 1.05 倍

导入:PyLongWriter_Create() 与 gmpy2

代码

static PyObject *
GMPy_PyLong_From_MPZ(MPZ_Object *obj, CTXT_Object *context)
{
    if (mpz_fits_slong_p(obj->z)) {
        return PyLong_FromLong(mpz_get_si(obj->z));
    }

    size_t size = (mpz_sizeinbase(obj->z, 2) +
                   int_bits_per_digit - 1) / int_bits_per_digit;
    void *digits;
    PyLongWriter *writer = PyLongWriter_Create(mpz_sgn(obj->z) < 0, size,
                                               &digits);
    if (writer == NULL) {
        return NULL;
    }

    mpz_export(digits, NULL, int_digits_order, int_digit_size,
               int_endianness, int_nails, obj->z);

    return PyLongWriter_Finish(writer);
}

参考代码:gmpy2 master 中 commit 9177648 的 GMPy_PyLong_From_MPZ()

基准测试

import pyperf
from gmpy2 import mpz

runner = pyperf.Runner()
runner.bench_func('1<<7', int, mpz(1 << 7))
runner.bench_func('1<<38', int, mpz(1 << 38))
runner.bench_func('1<<300', int, mpz(1 << 300))
runner.bench_func('1<<3000', int, mpz(1 << 3000))

在 Linux Fedora 40 上,CPU 隔离,Python 在发布模式下构建的结果

基准测试 参考 pep757
1<<7 56.7 纳秒 56.2 纳秒:快 1.01 倍
1<<300 191 纳秒 213 纳秒:慢 1.12 倍
几何平均值 (参考) 慢 1.03 倍

基准测试被隐藏,因为不显著(2):1<<38, 1<<3000。

向后兼容性

对向后兼容性没有影响,只添加了新的 API。

被拒绝的想法

支持任意布局

支持任意布局以导入-导出 Python 整数将很方便。

例如,曾提议向 PyLongWriter_Create() 添加一个 layout 参数,并向 PyLongExport 结构添加一个 layout 成员。

问题在于,它的实现更复杂,而且并非真正需要。严格来说,只需要一个 API 来使用 Python“原生”布局进行导入-导出。

如果以后有任意布局的用例,可以添加新的 API。

不添加PyLong_GetNativeLayout() 函数

目前,大多数 int 导入/导出所需的信息已通过 PyLong_GetInfo()(和 sys.int_info)提供。我们还可以添加更多信息(例如数字的顺序),此接口不会对 PyLongObject 的未来演变施加任何限制。

问题在于 PyLong_GetInfo() 返回一个 Python 对象,即命名元组,而不是一个方便的 C 结构,这可能会让人们倾向于使用当前的半私有宏,例如 PyLong_SHIFTPyLong_BASE

提供类似 mpz_import/export 的 API

另一种从 int 对象导入/导出数据的方法是:期望 C 扩展提供连续缓冲区,然后 CPython 导出(或导入)整数的绝对值。

API 示例

struct PyLongLayout {
    uint8_t bits_per_digit;
    uint8_t digit_size;
    int8_t digits_order;
};

size_t PyLong_GetDigitsNeeded(PyLongObject *obj, PyLongLayout layout);
int PyLong_Export(PyLongObject *obj, PyLongLayout layout, void *buffer);
PyLongObject *PyLong_Import(PyLongLayout layout, void *buffer);

这可能适用于 GMP,因为它有 mpz_limbs_read()mpz_limbs_write() 函数,可以提供对 mpz_t 内部的所需访问。其他库可能需要使用临时缓冲区,然后在它们那边使用类似 mpz_import/export 的函数。

这种方法的主要缺点是它在 CPython 端要复杂得多(即不同布局之间的实际转换)。例如,CPython 中 PyLong_FromNativeBytes()PyLong_AsNativeBytes() 的实现(共同提供了所需 API 的受限版本)大约需要 500 行代码(而当前实现大约需要 100 行)。

从导出 API 中删除value 字段

通过此建议,将只存在一种导出类型(“数字”数组)。如果给定整数没有这样的视图,它将由导出函数模拟,或者 PyLong_Export() 将返回错误。在两种情况下,都假设用户将使用其他 C-API 函数来获取“足够小”的整数(即,适合某些机器整数类型),例如 PyLong_AsLongAndOverflow()。在这种情况下,PyLong_Export() 将是低效的(或者只是失败)。

一个例子

static int
mpz_set_PyLong(mpz_t z, PyObject *obj)
{
    int overflow;
#if SIZEOF_LONG == 8
    long value = PyLong_AsLongAndOverflow(obj, &overflow);
#else
    /* Windows has 32-bit long, so use 64-bit long long instead */
    long long value = PyLong_AsLongLongAndOverflow(obj, &overflow);
#endif
    Py_BUILD_ASSERT(sizeof(value) == sizeof(int64_t));

    if (!overflow) {
        if (LONG_MIN <= value && value <= LONG_MAX) {
            mpz_set_si(z, (long)value);
        }
        else {
            mpz_import(z, 1, -1, sizeof(int64_t), 0, 0, &value);
            if (value < 0) {
                mpz_t tmp;
                mpz_init(tmp);
                mpz_ui_pow_ui(tmp, 2, 64);
                mpz_sub(z, z, tmp);
                mpz_clear(tmp);
            }
        }

    }
    else {
        static PyLongExport long_export;

        if (PyLong_Export(obj, &long_export) < 0) {
            return -1;
        }
        mpz_import(z, long_export.ndigits, int_digits_order, int_digit_size,
                   int_endianness, int_nails, long_export.digits);
        if (long_export.negative) {
            mpz_neg(z, z);
        }
        PyLong_FreeExport(&long_export);
    }
    return 0;
}

这从 API 设计者的角度来看可能是一种简化,但对最终用户来说会不太方便。他们将不得不跟踪 Python 的发展,对导出小整数的不同变体进行基准测试(为什么选择上述情况而不是 PyLong_AsInt64()?),可能还需要支持各种 CPython 版本或不同 Python 实现的不同代码路径。

讨论


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

上次修改:2024-12-16 07:23:59 GMT