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>
PEP 代表:
C API 工作组
讨论列表:
Discourse 线程
状态:
草稿
类型:
标准跟踪
创建:
2024 年 9 月 13 日
Python 版本:
3.14
历史记录:
2024 年 9 月 14 日

目录

摘要

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

理由

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 更改其内部结构,而不会破坏这些项目。例如,最近为 CPython 3.9 和 CPython 3.12 更改了 gmpy2 的实现。

规范

布局 API

API

typedef struct PyLongLayout {
    // Bits per digit
    uint8_t bits_per_digit;

    // Digit size in bytes
    uint8_t digit_size;

    // Digits order:
    // * 1 for most significant digit first
    // * -1 for least significant digit first
    int8_t digits_order;

    // Endian:
    // * 1 for most significant byte first (big endian)
    // * -1 for least significant byte first (little endian)
    int8_t endian;
} PyLongLayout;

PyAPI_FUNC(const PyLongLayout*) PyLong_GetNativeLayout(void);

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

PyLong_GetNativeLayout()

API

const PyLongLayout* PyLong_GetNativeLayout(void)

获取 Python int 对象的原生布局。

导出 API

将 Python 整数导出为数字数组

typedef struct PyLong_DigitArray {
    // Strong reference to the Python int object.
    PyObject *obj;

    // 1 if the number is negative, 0 otherwise.
    int negative;

    // Number of digits in the 'digits' array.
    Py_ssize_t ndigits;

    // Read-only array of unsigned digits.
    const void *digits;
} PyLong_DigitArray;

PyAPI_FUNC(int) PyLong_AsDigitArray(
    PyObject *obj,
    PyLong_DigitArray *array);
PyAPI_FUNC(void) PyLong_FreeDigitArray(
    PyLong_DigitArray *array);

在 CPython 3.14 中,不需要内存复制,它只是一个简单的包装器,用于公开 Python int 内部数字数组。

PyLong_DigitArray.obj 存储对 Python int 对象的强引用,以确保该结构在调用 PyLong_FreeDigitArray() 之前保持有效。

PyLong_AsDigitArray()

API

int PyLong_AsDigitArray(PyObject *obj, PyLong_DigitArray *array)

将 Python int 对象导出为数字数组。

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

如果 obj 是 Python int 对象或其子类,则此函数始终成功。

PyLong_FreeDigitArray() 必须在使用完 array 后调用一次。

PyLong_FreeDigitArray()

API

void PyLong_FreeDigitArray(PyLong_DigitArray *array)

释放由 PyLong_AsDigitArray() 创建的导出 array

导入 API

从数字数组导入 Python 整数

// A Python integer writer instance.
// The instance must be destroyed by PyLongWriter_Finish().
typedef struct PyLongWriter PyLongWriter;

PyAPI_FUNC(PyLongWriter*) PyLongWriter_Create(
    int negative,
    Py_ssize_t ndigits,
    void **digits);
PyAPI_FUNC(PyObject*) PyLongWriter_Finish(PyLongWriter *writer);
PyAPI_FUNC(void) PyLongWriter_Discard(PyLongWriter *writer);

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

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

PyLongWriter_Create()

API

PyLongWriter* PyLongWriter_Create(int negative, Py_ssize_t ndigits, void **digits)

创建一个 PyLongWriter

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

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

ndigitsdigits 数组中的数字个数。它必须大于或等于 0。

调用者必须初始化数字数组 digits,然后调用 PyLongWriter_Finish() 以获取 Python int。数字必须在 [0; PyLong_BASE - 1] 范围内。未使用的数字必须设置为 0

PyLongWriter_Finish()

API

PyObject* PyLongWriter_Finish(PyLongWriter *writer)

完成由 PyLongWriter_Create() 创建的 PyLongWriter

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

PyLongWriter_Discard()

API

void PyLongWriter_Discard(PyLongWriter *writer)

丢弃内部对象并销毁写入器实例。

优化小整数

提议的 API 对于大整数来说是高效的。与直接访问 Python 内部结构相比,提议的 API 在小整数上可能会产生明显的性能开销。

对于少数几个数字(例如 1 或 2 个数字)的小整数,可以使用现有的 API。导入/导出示例

实现

基准测试

导出:使用 gmpy2 的 PyLong_AsDigitArray()

代码

static void
mpz_set_PyLong(mpz_t z, PyObject *obj)
{
    int overflow;
    long val = PyLong_AsLongAndOverflow(obj, &overflow);

    if (overflow) {
        const PyLongLayout* layout = PyLong_GetNativeLayout();
        static PyLong_DigitArray long_export;

        PyLong_AsDigitArray(obj, &long_export);
        mpz_import(z, long_export.ndigits, layout->endian,
                   layout->digit_size, layout->digits_order,
                   layout->digit_size*8 - layout->bits_per_digit,
                   long_export.digits);
        if (long_export.negative) {
            mpz_neg(z, z);
        }
        PyLong_FreeDigitArray(&long_export);
    }
    else {
        mpz_set_si(z, val);
    }
}

基准测试

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)

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

基准测试 参考 pep757
1<<7 94.3 ns 96.8 ns:慢 1.03 倍
1<<38 127 ns 99.7 ns:快 1.28 倍
1<<300 209 ns 222 ns:慢 1.06 倍
1<<3000 955 ns 963 ns:慢 1.01 倍
几何平均数 (参考) 快 1.04 倍

导入:使用 gmpy2 的 PyLongWriter_Create()

代码

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));
    }

    const PyLongLayout *layout = PyLong_GetNativeLayout();
    size_t size = (mpz_sizeinbase(obj->z, 2) +
                   layout->bits_per_digit - 1) / layout->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, layout->endian,
               layout->digit_size, layout->digits_order,
               layout->digit_size*8 - layout->bits_per_digit,
               obj->z);

    return PyLongWriter_Finish(writer);
}

基准测试

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))

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

基准测试 参考 pep757
1<<300 193 ns 215 ns:慢 1.11 倍
1<<3000 927 ns 943 ns:慢 1.02 倍
几何平均数 (参考) 慢 1.03 倍

基准测试隐藏,因为不重要(2):1<<7,1<<38。

向后兼容性

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

未解决的问题

  • 是否应该在 sys.int_info 中添加 digits_orderendian 成员,并移除 PyLong_GetNativeLayout()PyLong_GetNativeLayout() 函数返回一个 C 结构,在 C 中使用比 sys.int_info(使用 Python 对象)更方便。
  • 目前,int 导入/导出所需的所有信息都可以通过 PyLong_GetInfo()sys.int_info 获取。“数字”的原生字节序和当前数字顺序(最低有效数字优先)——是所有任意精度整数算术库的共同点。因此,我们是否应该从 API 中移除 PyLongLayoutPyLong_GetNativeLayout()(实际上只是一个小的便利)?

被拒绝的想法

支持任意布局

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

例如,有人提议在 PyLongWriter_Create() 中添加一个 layout 参数,并在 PyLong_DigitArray 结构中添加一个 layout 成员。

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

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

讨论


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

上次修改:2024-09-14 12:26:39 GMT