PEP 757 – Python 정수를 가져오고 내보내기 위한 C API
- Author:
- Sergey B Kirpichev <skirpichev at gmail.com>, Victor Stinner <vstinner at python.org>
- Discussions-To:
- Discourse thread
- Status:
- Final
- Type:
- Standards Track
- Created:
- 13-Sep-2024
- Python-Version:
- 3.14
- Post-History:
- 14-Sep-2024
- Resolution:
- 08-Dec-2024
Table of Contents
- 초록
- 근거
- 사양
- 작은 정수 가져오기 최적화
- 구현
- 벤치마크
- 내보내기:
PyLong_Export()를 gmpy2와 함께 사용 - 가져오기:
PyLongWriter_Create()를 gmpy2와 함께 사용
- 내보내기:
- 하위 호환성
- 거부된 아이디어
- 임의 레이아웃 지원
- 관련
PyLong_GetNativeLayout()함수를 추가하지 마십시오. - 대신 mpz_import/export와 유사한 API 제공
- 내보내기 API에서
value필드를 제거합니다
- 논의
- Copyright
번역·라이선스 안내
이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain or CC0-1.0, whichever is more permissive 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판
초록
Python 정수인 int 객체를 가져오고 내보내기 위한 새로운 C API를 추가합니다. 특히 PyLongWriter_Create() 및 PyLong_Export() 함수를 추가합니다.
근거
gmpy2, SAGE 및 Python-FLINT와 같은 프로젝트는 Python int 객체를 가져오고 내보내기 위해 Python “내부”( PyLongObject 구조)에 직접 접근하거나 비효율적인 임시 형식(Python-FLINT의 경우 16진수 문자열)을 사용합니다. 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와 같은 import–export 함수에 필요한 데이터입니다.
-
struct PyLongLayout
- Layout of an array of “digits” (“limbs” in the GMP terminology), used to
represent absolute value for arbitrary precision integers.
Use
PyLong_GetNativeLayout()to get the native layout of Pythonintobjects, used internally for integers with “big enough” absolute value.See also
sys.int_infowhich exposes similar information to Python.-
uint8_t bits_per_digit
- Bits per digit. For example, a 15 bit digit means that bits 0-14 contain meaningful information.
-
uint8_t digit_size
- Digit size in bytes. For example, a 15 bit digit will require at least 2 bytes.
-
int8_t digits_order
- Digits order:
1for most significant digit first-1for least significant digit first
-
int8_t digit_endianness
- Digit endianness:
1for most significant byte first (big endian)-1for least significant byte first (little endian)
-
uint8_t bits_per_digit
-
const PyLongLayout *PyLong_GetNativeLayout(void)
- Get the native layout of Python
intobjects.See the
PyLongLayoutstructure.The function must not be called before Python initialization nor after Python finalization. The returned layout is valid until Python is finalized. The layout is the same for all Python sub-interpreters and so it can be cached.
내보내기 API
-
struct PyLongExport
- Export of a Python
intobject.There are two cases:
- If
digitsisNULL, only use thevaluemember. - If
digitsis notNULL, usenegative,ndigitsanddigitsmembers.
-
int64_t value
- The native integer value of the exported
intobject. Only valid ifdigitsisNULL.
-
uint8_t negative
- 1 if the number is negative, 0 otherwise.
Only valid if
digitsis notNULL.
-
Py_ssize_t ndigits
- Number of digits in
digitsarray. Only valid ifdigitsis notNULL.
-
const void *digits
- Read-only array of unsigned digits. Can be
NULL.
- If
해당 멤버 PyLongExport.digits가 NULL이 아닌 경우, PyLongExport 구조체의 비공개 필드가 Python int객체에 대한 강한 참조를 저장하여 PyLong_FreeExport()가 호출될 때까지 해당 구조체가 유효하게 유지되도록 합니다.
-
int PyLong_Export(PyObject *obj, PyLongExport *export_long)
- Export a Python
intobject.export_long must point to a
PyLongExportstructure allocated by the caller. It must not beNULL.On success, fill in *export_long and return 0. On error, set an exception and return -1.
PyLong_FreeExport()must be called when the export is no longer needed.CPython implementation detail: This function always succeeds if obj is a Python
intobject or a subclass.
CPython 3.14에서는 PyLong_Export()에서 메모리 복사가 필요하지 않으며, Python int의 내부 digits 배열을 노출하는 얇은 래퍼일 뿐입니다.
-
void PyLong_FreeExport(PyLongExport *export_long)
- Release the export export_long created by
PyLong_Export().CPython implementation detail: Calling
PyLong_FreeExport()is optional if export_long->digits isNULL.
가져오기 API
정수를 가져오는 데 PyLongWriter API를 사용할 수 있습니다.
-
struct PyLongWriter
- A Python
intwriter instance.The instance must be destroyed by
PyLongWriter_Finish()orPyLongWriter_Discard().
-
PyLongWriter *PyLongWriter_Create(int negative, Py_ssize_t ndigits, void **digits)
- Create a
PyLongWriter.On success, allocate *digits and return a writer. On error, set an exception and return
NULL.negative is
1if the number is negative, or0otherwise.ndigits is the number of digits in the digits array. It must be greater than 0.
digits must not be NULL.
After a successful call to this function, the caller should fill in the array of digits digits and then call
PyLongWriter_Finish()to get a Pythonint. The layout of digits is described byPyLong_GetNativeLayout().Digits must be in the range [
0;(1 << bits_per_digit) - 1] (where thebits_per_digitis the number of bits per digit). Any unused most significant digits must be set to0.Alternately, call
PyLongWriter_Discard()to destroy the writer instance without creating anintobject.
CPython 3.14에서 PyLongWriter_Create() 구현은 비공개 _PyLong_New() 함수에 대한 얇은 래퍼입니다.
-
PyObject *PyLongWriter_Finish(PyLongWriter *writer)
- Finish a
PyLongWritercreated byPyLongWriter_Create().On success, return a Python
intobject. On error, set an exception and returnNULL.The function takes care of normalizing the digits and converts the object to a compact integer if needed.
The writer instance and the digits array are invalid after the call.
-
void PyLongWriter_Discard(PyLongWriter *writer)
- Discard a
PyLongWritercreated byPyLongWriter_Create().writer must not be
NULL.The writer instance and the digits array are invalid after the call.
작은 정수 가져오기 최적화
제안된 가져오기 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;
}
참조 코드: mpz_set_PyLong() in the gmpy2 master for commit 9177648입니다.
벤치마크:
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 격리 환경에서 Python을 릴리스 모드로 빌드한 Linux Fedora 40에서의 결과:
| 벤치마크 | 참조 | pep757 |
|---|---|---|
| 1<<7 | 91.3 ns | 89.9 ns: 1.02배 빠름 |
| 1<<38 | 120 ns | 94.9 ns: 1.27배 빠름 |
| 1<<300 | 196 ns | 203 ns: 1.04배 느림 |
| 1<<3000 | 939 ns | 945 ns: 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);
}
참조 코드: GMPy_PyLong_From_MPZ() in the gmpy2 master for commit 9177648입니다.
벤치마크:
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 격리 환경에서 Python을 릴리스 모드로 빌드한 Linux Fedora 40에서의 결과:
| 벤치마크 | ref | pep757 |
|---|---|---|
| 1<<7 | 56.7 ns | 56.2 ns: 1.01배 빠릅니다 |
| 1<<300 | 191 ns | 213 ns: 1.12배 느립니다 |
| 기하 평균 | (ref) | 1.03배 느립니다 |
유의미하지 않아 벤치마크가 숨겨졌습니다(2): 1<<38, 1<<3000.
하위 호환성
하위 호환성에는 영향이 없으며, 새로운 API만 추가됩니다.
거부된 아이디어
임의 레이아웃 지원
Python 정수를 가져오고 내보낼 때 임의의 레이아웃을 지원하면 편리합니다.
예를 들어, PyLongWriter_Create()에 layout 매개변수를 추가하고 PyLongExport 구조체에 layout 멤버를 추가하자는 제안이 있었습니다.
문제는 구현이 더 복잡하고 실제로 필요하지 않다는 점입니다. 엄밀히 필요한 것은 Python의 “native” 레이아웃을 사용하여 가져오고 내보내는 API뿐입니다.
나중에 임의 레이아웃에 대한 사용 사례가 생기면 새로운 API를 추가할 수 있습니다.
관련 PyLong_GetNativeLayout() 함수를 추가하지 마십시오.
현재 int 가져오기/내보내기에 필요한 정보 대부분은 이미 PyLong_GetInfo()를 통해(그리고 sys.int_info를 통해) 사용할 수 있습니다. 더 많은 정보(예: 숫자의 순서)도 추가할 수 있으며, 이 인터페이스는 PyLongObject의 향후 발전에 어떠한 제약도 가하지 않습니다.
문제는 PyLong_GetInfo()가 편리한 C 구조체가 아닌 Python 객체인 named tuple을 반환한다는 점이며, 이로 인해 사람들이 현재의 반(半)비공개 매크로인 PyLong_SHIFT 및 PyLong_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의 내부에 필요한 접근을 제공할 수 있으므로 이 방식이 GMP에서 작동할 수 있습니다. 다른 라이브러리에서는 임시 버퍼를 사용한 다음 해당 라이브러리 측에서 mpz_import/export와 유사한 함수를 사용해야 할 수 있습니다.
이 접근 방식의 주요 단점은 CPython 측에서 훨씬 더 복잡하다는 점입니다(즉, 서로 다른 레이아웃 간의 실제 변환이 필요합니다). 예를 들어, CPython에서 PyLong_FromNativeBytes() 및 PyLong_AsNativeBytes()의 구현(둘을 함께 사용하면 필요한 API의 제한된 버전을 제공함)에는 약 500 LOC가 소요되었지만(현재 구현에서는 약 100 LOC), 비교하면 상당히 많습니다.
내보내기 API에서 value 필드를 제거합니다
이 제안에 따르면 내보내기 유형은 하나만 존재합니다(“digits” 배열). 주어진 정수에 이러한 뷰를 사용할 수 없는 경우, 내보내기 함수로 에뮬레이션하거나 PyLong_Export()은 오류를 반환합니다. 두 경우 모두 사용자가 일부 머신 정수 유형에 들어갈 만큼 “충분히 작은” 정수를 얻기 위해 PyLong_AsLongAndOverflow()처럼 다른 C API 함수를 사용할 것으로 가정합니다. 이 경우 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 구현 간에 서로 다른 코드 경로를 지원해야 할 수도 있습니다.
논의
- Discourse: PEP 757 – Python 정수 가져오기 및 내보내기를 위한 C API
- C API 작업 그룹 결정 이슈 #35
- 풀 리퀘스트 #121339
- 이슈 #102471: Python 정수를 C 정수로 변환하는 C API는 솔직히 말해 엉망입니다.
- 공개 함수 PyLong_GetDigits() 추가
- _PyLong_New() 함수를 공개 함수로 복원하는 것을 고려
- 풀 리퀘스트 gh-106320: 비공개 _PyLong_New() 함수를 제거합니다.
Copyright
This document is placed in the public domain or under the CC0-1.0-Universal license, whichever is more permissive.