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()
函数。
理由
像 gmpy2、SAGE 和 Python-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
。
如果数字为负,则 negative 为 1
,否则为 0
。
ndigits 是 digits 数组中的数字个数。它必须大于或等于 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_order 和 endian 成员,并移除PyLong_GetNativeLayout()
?PyLong_GetNativeLayout()
函数返回一个 C 结构,在 C 中使用比sys.int_info
(使用 Python 对象)更方便。 - 目前,
int
导入/导出所需的所有信息都可以通过PyLong_GetInfo()
或sys.int_info
获取。“数字”的原生字节序和当前数字顺序(最低有效数字优先)——是所有任意精度整数算术库的共同点。因此,我们是否应该从 API 中移除PyLongLayout
和PyLong_GetNativeLayout()
(实际上只是一个小的便利)?
被拒绝的想法
支持任意布局
支持任意布局来导入导出 Python 整数将会很方便。
例如,有人提议在 PyLongWriter_Create()
中添加一个 layout 参数,并在 PyLong_DigitArray
结构中添加一个 layout 成员。
问题在于实现起来更复杂,而且实际上并不需要。严格来说,只需要一个使用 Python “原生”布局进行导入导出的 API。
如果以后有任意布局的使用案例,可以添加新的 API。
讨论
版权
本文档已放置在公共领域或根据 CC0-1.0-Universal 许可证发布,以更宽松的许可证为准。
来源:https://github.com/python/peps/blob/main/peps/pep-0757.rst