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
- Zusammenfassung
- Begründung
- Spezifikation
- Import für kleine Ganzzahlen optimieren
- Implementierung
- Benchmarks
- Export:
PyLong_Export()mit gmpy2 - Import:
PyLongWriter_Create()mit gmpy2
- Export:
- Abwärtskompatibilität
- Abgelehnte Ideen
- Beliebige Layouts unterstützen
- Keine Funktion
PyLong_GetNativeLayout()hinzufügen - Stattdessen eine mpz_import/export-ähnliche API bereitstellen
- Feld
valueaus der Export-API entfernen
- Diskussionen
- Urheberrecht
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 Pythonint-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
1für die höchstwertige Ziffer zuerst-1für die niedrigstwertige Ziffer zuerst
-
int8_t digit_endianness
- Endianness der Ziffern
1für das höchstwertige Byte zuerst (Big-Endian)-1für das niedrigstwertige Byte zuerst (Little-Endian)
-
uint8_t bits_per_digit
-
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:
- Wenn
digitsNULList, verwenden Sie nur das Mitgliedvalue. - Wenn
digitsnichtNULList, verwenden Sie die Mitgliedernegative,ndigitsunddigits.
-
int64_t value
- Der native Ganzzahlwert des exportierten
int-Objekts. Nur gültig, wenndigitsNULList.
-
uint8_t negative
- 1, wenn die Zahl negativ ist, sonst 0. Nur gültig, wenn
digitsnichtNULList.
-
Py_ssize_t ndigits
- Anzahl der Ziffern im Array
digits. Nur gültig, wenndigitsnichtNULList.
-
const void *digits
- Schreibgeschütztes Array von unsigned digits. Kann
NULLsein.
- Wenn
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 nichtNULLsein.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->digitsNULList.
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()oderPyLongWriter_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
NULLzurückgegeben.negative ist
1, wenn die Zahl negativ ist, oder andernfalls0.ndigits ist die Anzahl der Ziffern im Array digits. Es muss größer als 0 sein.
digits darf nicht
NULLsein.Nach einem erfolgreichen Aufruf dieser Funktion sollte der Aufrufer das Array von Ziffern digits füllen und dann
PyLongWriter_Finish()aufrufen, um ein Pythonintzu erhalten. Das Layout von digits wird durchPyLong_GetNativeLayout()beschrieben.Die Ziffern müssen im Bereich [
0;(1 << bits_per_digit) - 1] liegen (wobeibits_per_digitdie Anzahl der Bits pro Ziffer ist). Alle nicht verwendeten höchstwertigen Ziffern müssen auf0gesetzt werden.Alternativ kann
PyLongWriter_Discard()aufgerufen werden, um die Writer-Instanz zu zerstören, ohne einint-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()erstelltenPyLongWriterab.Bei Erfolg wird ein Python
int-Objekt zurückgegeben. Bei einem Fehler wird eine Ausnahme gesetzt undNULLzurü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()erstelltenPyLongWriter.writer darf nicht
NULLsein.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
- Discourse: PEP 757 – C-API zum Importieren und Exportieren von Python-Ganzzahlen
- Entscheidung des C API Working Groups, Thema #35
- Pull Request #121339
- Issue #102471: Die C-API für die Konvertierung von Python zu C Ganzzahlen ist, offen gesagt, ein Durcheinander.
- Fügen Sie die öffentliche Funktion PyLong_GetDigits() hinzu
- Erwägen Sie, die Funktion _PyLong_New() wieder als öffentlich einzustufen
- Pull Request gh-106320: Entfernen der privaten Funktion _PyLong_New().
Urheberrecht
Dieses Dokument wird in die Public Domain oder unter die CC0-1.0-Universal-Lizenz gestellt, je nachdem, welche Lizenz permissiver ist.
Quelle: https://github.com/python/peps/blob/main/peps/pep-0757.rst
Zuletzt geändert: 2024-12-16 07:23:59 GMT