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

Python Enhancement Proposals

PEP 757 – C-API zum Importieren und Exportieren von Python-Ganzzahlen

Autor:
Sergey B Kirpichev <skirpichev at gmail.com>, Victor Stinner <vstinner at python.org>
Discussions-To:
Discourse thread
Status:
Final
Typ:
Standards Track
Erstellt:
13. Sep. 2024
Python-Version:
3.14
Post-History:
14. Sep. 2024
Resolution:
08. Dez. 2024

Inhaltsverzeichnis

Wichtig

Dieser PEP ist ein historisches Dokument. Die aktuelle, kanonische Dokumentation finden Sie jetzt in der Export-API und der PyLongWriter-API.

×

Siehe PEP 1, um Änderungen vorzuschlagen.

Zusammenfassung

Fügen Sie eine neue C-API zum Importieren und Exportieren von Python-Ganzzahlen, int-Objekten, hinzu: insbesondere die Funktionen PyLongWriter_Create() und PyLong_Export().

Begründung

Projekte wie gmpy2, SAGE und Python-FLINT greifen direkt auf Python-„Interna“ (die PyLongObject-Struktur) zu oder verwenden ein ineffizientes temporäres Format (Hex-Strings für Python-FLINT), um Python int-Objekte zu importieren und zu exportieren. Die Python int-Implementierung wurde in Python 3.12 geändert, um ein Tag und „kompakte Werte“ hinzuzufügen.

In der Version 3.13 Alpha 1 wurde die private, undokumentierte Funktion _PyLong_New() entfernt. Diese wurde jedoch von diesen Projekten zum Importieren von Python-Ganzzahlen verwendet. Die private Funktion wurde in 3.13 Alpha 2 wiederhergestellt.

Eine öffentliche, effiziente Abstraktion ist erforderlich, um Python mit diesen Projekten zu verbinden, ohne Implementierungsdetails preiszugeben. Dies würde es Python ermöglichen, seine Interna zu ändern, ohne diese Projekte zu brechen. Zum Beispiel wurde die Implementierung für gmpy2 kürzlich für CPython 3.9 und für CPython 3.12 geändert.

Spezifikation

Layout-API

Daten, die von GMP-ähnlichen Import- und Export-Funktionen benötigt werden.

struct PyLongLayout
Layout eines Arrays von „Ziffern“ („Limbs“ in der GMP-Terminologie), das zur Darstellung des Absolutwerts für Ganzzahlen mit beliebiger Genauigkeit verwendet wird.

Verwenden Sie PyLong_GetNativeLayout(), um das native Layout von Python int-Objekten zu erhalten, das intern für Ganzzahlen mit „ausreichend großen“ Absolutwerten verwendet wird.

Siehe auch sys.int_info, das ähnliche Informationen für Python verfügbar macht.

uint8_t bits_per_digit
Bits pro Ziffer. Eine 15-Bit-Ziffer bedeutet beispielsweise, dass die Bits 0-14 aussagekräftige Informationen enthalten.
uint8_t digit_size
Zifferngröße in Bytes. Eine 15-Bit-Ziffer benötigt beispielsweise mindestens 2 Bytes.
int8_t digits_order
Reihenfolge der Ziffern
  • 1 für die höchstwertige Ziffer zuerst
  • -1 für die niedrigstwertige Ziffer zuerst
int8_t digit_endianness
Endianness der Ziffern
  • 1 für das höchstwertige Byte zuerst (Big-Endian)
  • -1 für das niedrigstwertige Byte zuerst (Little-Endian)
const PyLongLayout *PyLong_GetNativeLayout(void)
Ruft das native Layout von Python int-Objekten ab.

Siehe die Struktur PyLongLayout.

Die Funktion darf nicht vor der Python-Initialisierung oder nach der Python-Finalisierung aufgerufen werden. Das zurückgegebene Layout ist bis zur Python-Finalisierung gültig. Das Layout ist für alle Python-Subinterpreter gleich und kann daher gecached werden.

Export-API

struct PyLongExport
Export eines Python int-Objekts.

Es gibt zwei Fälle:

int64_t value
Der native Ganzzahlwert des exportierten int-Objekts. Nur gültig, wenn digits NULL ist.
uint8_t negative
1, wenn die Zahl negativ ist, sonst 0. Nur gültig, wenn digits nicht NULL ist.
Py_ssize_t ndigits
Anzahl der Ziffern im Array digits. Nur gültig, wenn digits nicht NULL ist.
const void *digits
Schreibgeschütztes Array von unsigned digits. Kann NULL sein.

Wenn PyLongExport.digits nicht NULL ist, speichert ein privates Feld der Struktur PyLongExport eine starke Referenz auf das Python int-Objekt, um sicherzustellen, dass diese Struktur gültig bleibt, bis PyLong_FreeExport() aufgerufen wird.

int PyLong_Export(PyObject *obj, PyLongExport *export_long)
Exportiert ein Python int-Objekt.

export_long muss auf eine von der aufrufenden Partei zugewiesene PyLongExport-Struktur zeigen. Sie darf nicht NULL sein.

Bei Erfolg wird *export_long* gefüllt und 0 zurückgegeben. Bei einem Fehler wird eine Ausnahme gesetzt und -1 zurückgegeben.

PyLong_FreeExport() muss aufgerufen werden, wenn der Export nicht mehr benötigt wird.

CPython-Implementierungsdetail: Diese Funktion ist immer erfolgreich, wenn obj ein Python int-Objekt oder eine Unterklasse ist.

In CPython 3.14 ist keine Speicher kopiert erforderlich in PyLong_Export(), es ist nur ein dünner Wrapper, um das interne Ziffernarray von Python int freizugeben.

void PyLong_FreeExport(PyLongExport *export_long)
Gibt den von PyLong_Export() erstellten Export export_long frei.

CPython-Implementierungsdetail: Der Aufruf von PyLong_FreeExport() ist optional, wenn export_long->digits NULL ist.

Import-API

Die PyLongWriter-API kann verwendet werden, um eine Ganzzahl zu importieren.

struct PyLongWriter
Eine Python int-Writer-Instanz.

Die Instanz muss von PyLongWriter_Finish() oder PyLongWriter_Discard() zerstört werden.

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

Bei Erfolg wird *digits zugewiesen und ein Writer zurückgegeben. Bei einem Fehler wird eine Ausnahme gesetzt und NULL zurückgegeben.

negative ist 1, wenn die Zahl negativ ist, oder andernfalls 0.

ndigits ist die Anzahl der Ziffern im Array digits. Es muss größer als 0 sein.

digits darf nicht NULL sein.

Nach einem erfolgreichen Aufruf dieser Funktion sollte der Aufrufer das Array von Ziffern digits füllen und dann PyLongWriter_Finish() aufrufen, um ein Python int zu erhalten. Das Layout von digits wird durch PyLong_GetNativeLayout() beschrieben.

Die Ziffern müssen im Bereich [0; (1 << bits_per_digit) - 1] liegen (wobei bits_per_digit die Anzahl der Bits pro Ziffer ist). Alle nicht verwendeten höchstwertigen Ziffern müssen auf 0 gesetzt werden.

Alternativ kann PyLongWriter_Discard() aufgerufen werden, um die Writer-Instanz zu zerstören, ohne ein int-Objekt zu erstellen.

In CPython 3.14 ist die Implementierung von PyLongWriter_Create() ein dünner Wrapper für die private Funktion _PyLong_New().

PyObject *PyLongWriter_Finish(PyLongWriter *writer)
Schließt einen von PyLongWriter_Create() erstellten PyLongWriter ab.

Bei Erfolg wird ein Python int-Objekt zurückgegeben. Bei einem Fehler wird eine Ausnahme gesetzt und NULL zurückgegeben.

Die Funktion kümmert sich um die Normalisierung der Ziffern und wandelt das Objekt bei Bedarf in eine kompakte Ganzzahl um.

Die Writer-Instanz und das digits-Array sind nach dem Aufruf ungültig.

void PyLongWriter_Discard(PyLongWriter *writer)
Verwirft einen von PyLongWriter_Create() erstellten PyLongWriter.

writer darf nicht NULL sein.

Die Writer-Instanz und das digits-Array sind nach dem Aufruf ungültig.

Import für kleine Ganzzahlen optimieren

Die vorgeschlagene Import-API ist effizient für große Ganzzahlen. Im Vergleich zum direkten Zugriff auf Python-Interna kann die vorgeschlagene Import-API bei kleinen Ganzzahlen einen erheblichen Performance-Overhead verursachen.

Für kleine Ganzzahlen mit wenigen Ziffern (z.B. 1 oder 2 Ziffern) können vorhandene APIs verwendet werden:

Implementierung

Benchmarks

Code

/* 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;

Export: PyLong_Export() mit gmpy2

Code

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

Referenzcode: mpz_set_PyLong() im gmpy2-Master für Commit 9177648.

Benchmark

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)

Ergebnisse unter Linux Fedora 40 mit CPU-Isolation, Python im Release-Modus gebaut

Benchmark ref pep757
1<<7 91.3 ns 89.9 ns: 1.02x schneller
1<<38 120 ns 94.9 ns: 1.27x schneller
1<<300 196 ns 203 ns: 1.04x langsamer
1<<3000 939 ns 945 ns: 1.01x langsamer
Geometrischer Mittelwert (ref) 1.05x schneller

Import: PyLongWriter_Create() mit gmpy2

Code

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

Referenzcode: GMPy_PyLong_From_MPZ() im gmpy2-Master für Commit 9177648.

Benchmark

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

Ergebnisse unter Linux Fedora 40 mit CPU-Isolation, Python im Release-Modus gebaut

Benchmark ref pep757
1<<7 56.7 ns 56.2 ns: 1.01x schneller
1<<300 191 ns 213 ns: 1.12x langsamer
Geometrischer Mittelwert (ref) 1.03x langsamer

Benchmark verborgen, da nicht signifikant (2): 1<<38, 1<<3000.

Abwärtskompatibilität

Es gibt keine Auswirkungen auf die Abwärtskompatibilität, es werden nur neue APIs hinzugefügt.

Abgelehnte Ideen

Beliebige Layouts unterstützen

Es wäre praktisch, beliebige Layouts zum Importieren/Exportieren von Python-Ganzzahlen zu unterstützen.

Zum Beispiel wurde vorgeschlagen, einen layout-Parameter zu PyLongWriter_Create() und ein layout-Mitglied zur Struktur PyLongExport hinzuzufügen.

Das Problem ist, dass die Implementierung komplexer ist und nicht wirklich benötigt wird. Was unbedingt benötigt wird, ist nur eine API zum Importieren und Exportieren unter Verwendung des Python „nativen“ Layouts.

Wenn später Anwendungsfälle für beliebige Layouts auftreten, können neue APIs hinzugefügt werden.

Keine Funktion PyLong_GetNativeLayout() hinzufügen

Derzeit sind die meisten benötigten Informationen für den int-Import/Export über PyLong_GetInfo() (und sys.int_info) verfügbar. Wir können auch mehr hinzufügen (wie die Reihenfolge der Ziffern), diese Schnittstelle stellt keine Einschränkungen für die zukünftige Entwicklung von PyLongObject dar.

Das Problem ist, dass PyLong_GetInfo() ein Python-Objekt, ein named tuple, zurückgibt und kein praktisches C-Struktur, was Leute davon abhalten könnte, es zu verwenden, zugunsten von z.B. aktuellen halb-privaten Makros wie PyLong_SHIFT und PyLong_BASE.

Stattdessen eine mpz_import/export-ähnliche API bereitstellen

Der andere Ansatz zum Importieren/Exportieren von Daten aus int-Objekten könnte der folgende sein: Erwarten Sie, dass C-Erweiterungen zusammenhängende Puffer bereitstellen, die CPython dann exportiert (oder importiert) den *Absolutwert* einer Ganzzahl.

API-Beispiel

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

Dies könnte für GMP funktionieren, da es die Funktionen mpz_limbs_read() und mpz_limbs_write() gibt, die Zugriff auf die Interna von mpz_t ermöglichen. Andere Bibliotheken erfordern möglicherweise die Verwendung von temporären Puffern und dann mpz_import/export-ähnliche Funktionen auf ihrer Seite.

Der Hauptnachteil dieses Ansatzes ist, dass er auf der CPython-Seite viel komplexer ist (d.h. die eigentliche Konvertierung zwischen verschiedenen Layouts). Zum Beispiel benötigte die Implementierung von PyLong_FromNativeBytes() und PyLong_AsNativeBytes() (zusammen eine eingeschränkte Version der erforderlichen API) in CPython ca. 500 Zeilen Code (verglichen mit ~100 Zeilen in der aktuellen Implementierung).

Feld value aus der Export-API entfernen

Mit diesem Vorschlag gäbe es nur einen Exporttyp (Array von „Ziffern“). Wenn eine solche Ansicht für eine gegebene Ganzzahl nicht verfügbar ist, wird sie entweder durch Exportfunktionen emuliert oder PyLong_Export() gibt einen Fehler zurück. In beiden Fällen wird davon ausgegangen, dass Benutzer andere C-API-Funktionen verwenden, um „kleine genug“ Ganzzahlen zu erhalten (d.h. die in einige Maschinenganzzahltypen passen), wie z.B. PyLong_AsLongAndOverflow(). PyLong_Export() wäre in diesem Fall ineffizient (oder schlägt einfach fehl).

Ein Beispiel

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

Dies mag aus Sicht des API-Designers wie eine Vereinfachung erscheinen, ist aber für Endbenutzer weniger praktisch. Sie müssten die Python-Entwicklung verfolgen, verschiedene Varianten für den Export kleiner Ganzzahlen benchmarken (ist es offensichtlich, warum der obige Fall anstelle von PyLong_AsInt64() gewählt wurde?), vielleicht unterschiedliche Code-Pfade für verschiedene CPython-Versionen oder über verschiedene Python-Implementierungen hinweg unterstützen.

Diskussionen


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

Zuletzt geändert: 2024-12-16 07:23:59 GMT