Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python 개선 제안 한국어 번역

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

번역·라이선스 안내

이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain or CC0-1.0, whichever is more permissive 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판

Important

This PEP is a historical document. The up-to-date, canonical documentation can now be found at the Export API and the PyLongWriter API.

×

See PEP 1 for how to propose changes.

초록

Python 정수인 int 객체를 가져오고 내보내기 위한 새로운 C API를 추가합니다. 특히 PyLongWriter_Create()PyLong_Export() 함수를 추가합니다.

근거

gmpy2, SAGEPython-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와 같은 importexport 함수에 필요한 데이터입니다.

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 Python int objects, used internally for integers with “big enough” absolute value.

See also sys.int_info which 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:
  • 1 for most significant digit first
  • -1 for least significant digit first
int8_t digit_endianness
Digit endianness:
  • 1 for most significant byte first (big endian)
  • -1 for least significant byte first (little endian)
const PyLongLayout *PyLong_GetNativeLayout(void)
Get the native layout of Python int objects.

See the PyLongLayout structure.

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 int object.

There are two cases:

int64_t value
The native integer value of the exported int object. Only valid if digits is NULL.
uint8_t negative
1 if the number is negative, 0 otherwise. Only valid if digits is not NULL.
Py_ssize_t ndigits
Number of digits in digits array. Only valid if digits is not NULL.
const void *digits
Read-only array of unsigned digits. Can be NULL.

해당 멤버 PyLongExport.digitsNULL이 아닌 경우, PyLongExport 구조체의 비공개 필드가 Python int객체에 대한 강한 참조를 저장하여 PyLong_FreeExport()가 호출될 때까지 해당 구조체가 유효하게 유지되도록 합니다.

int PyLong_Export(PyObject *obj, PyLongExport *export_long)
Export a Python int object.

export_long must point to a PyLongExport structure allocated by the caller. It must not be NULL.

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 int object 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 is NULL.

가져오기 API

정수를 가져오는 데 PyLongWriter API를 사용할 수 있습니다.

struct PyLongWriter
A Python int writer instance.

The instance must be destroyed by PyLongWriter_Finish() or PyLongWriter_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 1 if the number is negative, or 0 otherwise.

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 Python int. The layout of digits is described by PyLong_GetNativeLayout().

Digits must be in the range [0; (1 << bits_per_digit) - 1] (where the bits_per_digit is the number of bits per digit). Any unused most significant digits must be set to 0.

Alternately, call PyLongWriter_Discard() to destroy the writer instance without creating an int object.

CPython 3.14에서 PyLongWriter_Create() 구현은 비공개 _PyLong_New() 함수에 대한 얇은 래퍼입니다.

PyObject *PyLongWriter_Finish(PyLongWriter *writer)
Finish a PyLongWriter created by PyLongWriter_Create().

On success, return a Python int object. On error, set an exception and return NULL.

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 PyLongWriter created by PyLongWriter_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_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의 내부에 필요한 접근을 제공할 수 있으므로 이 방식이 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 구현 간에 서로 다른 코드 경로를 지원해야 할 수도 있습니다.

논의