PEP 353 – Verwendung von ssize_t als Index-Typ
- Autor:
- Martin von Löwis <martin at v.loewis.de>
- Status:
- Final
- Typ:
- Standards Track
- Erstellt:
- 18. Dez. 2005
- Python-Version:
- 2.5
- Post-History:
Zusammenfassung
In Python 2.4 sind Indizes von Sequenzen auf den C-Typ int beschränkt. Auf 64-Bit-Maschinen sind Sequenzen daher auf 2**31 Elemente beschränkt. Dieses PEP schlägt vor, dies zu ändern und einen plattformspezifischen Index-Typ Py_ssize_t einzuführen. Eine Implementierung der vorgeschlagenen Änderung finden Sie unter http://svn.python.org/projects/python/branches/ssize_t.
Begründung
64-Bit-Maschinen werden immer beliebter und der Hauptspeicher wächst über 4 GiB hinaus. Auf solchen Maschinen ist Python derzeit eingeschränkt, da Sequenzen (Strings, Unicode-Objekte, Tupel, Listen, array.arrays usw.) nicht mehr als 2 GiElemente enthalten können.
Heutzutage haben nur sehr wenige Maschinen genug Speicher, um größere Listen darzustellen: Da jeder Zeiger 8 Byte groß ist (auf einer 64-Bit-Maschine), benötigt man 16 GiB, um nur die Zeiger einer solchen Liste zu speichern; mit den Daten in der Liste wächst der Speicherverbrauch noch weiter. Es gibt jedoch drei Containertypen, für die Benutzer heute Verbesserungen wünschen
- Strings (derzeit auf 2 GiB beschränkt)
- mmap-Objekte (ebenso; plus das System hält das gesamte Objekt normalerweise nicht gleichzeitig im Speicher)
- Numarray-Objekte (von Numerical Python)
Da die vorgeschlagene Änderung zu Inkompatibilitäten auf 64-Bit-Maschinen führen wird, sollte sie durchgeführt werden, solange solche Maschinen noch nicht weit verbreitet sind (d. h. so früh wie möglich).
Spezifikation
Ein neuer Typ Py_ssize_t wird eingeführt, der die gleiche Größe wie der size_t-Typ des Compilers hat, aber vorzeichenbehaftet ist. Er wird, wo verfügbar, ein Typedef für ssize_t sein.
Die interne Darstellung der Längenfelder aller Containertypen wird von int auf ssize_t geändert, für alle Typen, die in der Standardverteilung enthalten sind. Insbesondere wird PyObject_VAR_HEAD so geändert, dass es Py_ssize_t verwendet, was alle Erweiterungsmodule betrifft, die dieses Makro verwenden.
Alle Vorkommen von Index- und Längenparametern und -ergebnissen werden auf Py_ssize_t geändert, einschließlich der Sequenz-Slots in Typobjekten und der Buffer-Schnittstelle.
Neue Konvertierungsfunktionen PyInt_FromSsize_t und PyInt_AsSsize_t werden eingeführt. PyInt_FromSsize_t gibt transparent ein Long-Int-Objekt zurück, wenn der Wert den LONG_MAX überschreitet; PyInt_AsSsize_t verarbeitet transparent Long-Int-Objekte.
Neue Funktionszeiger-Typedefs ssizeargfunc, ssizessizeargfunc, ssizeobjargproc, ssizessizeobjargproc und lenfunc werden eingeführt. Die Funktionstypen der Buffer-Schnittstelle werden nun readbufferproc, writebufferproc, segcountproc und charbufferproc genannt.
Ein neuer Konvertierungscode "n" wird für PyArg_ParseTuple, Py_BuildValue, PyObject_CallFunction und PyObject_CallMethod eingeführt. Dieser Code arbeitet mit Py_ssize_t.
Die Konvertierungscodes "s#" und "t#" geben Py_ssize_t aus, wenn das Makro PY_SSIZE_T_CLEAN vor der Einbindung von Python.h definiert ist, und geben weiterhin int aus, wenn dieses Makro nicht definiert ist.
An Stellen, an denen eine Konvertierung von size_t/Py_ssize_t zu int erforderlich ist, wird die Strategie für die Konvertierung fallweise gewählt (siehe nächster Abschnitt).
Um zu verhindern, dass Erweiterungsmodule, die einen 32-Bit-Größentyp annehmen, in einen Interpreter geladen werden, der einen 64-Bit-Größentyp hat, wird Py_InitModule4 in Py_InitModule4_64 umbenannt.
Konversionsrichtlinien
Modulautoren haben die Wahl, ob sie dieses PEP in ihrem Code unterstützen wollen oder nicht; wenn sie es unterstützen, haben sie die Wahl zwischen verschiedenen Kompatibilitätsstufen.
Wenn ein Modul nicht konvertiert wird, um dieses PEP zu unterstützen, funktioniert es auf einem 32-Bit-System unverändert. Auf einem 64-Bit-System können Kompilierungsfehler und Warnungen ausgegeben werden, und das Modul kann den Interpreter abstürzen lassen, wenn die Warnungen ignoriert werden.
Die Konvertierung eines Moduls kann entweder versuchen, weiterhin int-Indizes zu verwenden, oder Py_ssize_t-Indizes durchgängig zu verwenden.
Wenn das Modul weiterhin int-Indizes verwenden soll, ist Vorsicht geboten, wenn Funktionen aufgerufen werden, die Py_ssize_t oder size_t zurückgeben, insbesondere bei Funktionen, die die Länge eines Objekts zurückgeben (dazu gehören die strlen-Funktion und der sizeof-Operator). Ein guter Compiler warnt, wenn ein Py_ssize_t/size_t-Wert in ein int abgeschnitten wird. In diesen Fällen stehen drei Strategien zur Verfügung:
- statisch feststellen, dass die Größe niemals ein int überschreiten kann (z. B. beim sizeof einer Struktur oder dem strlen eines Dateipfads). In diesem Fall schreiben Sie
some_int = Py_SAFE_DOWNCAST(some_value, Py_ssize_t, int);
Dies fügt im Debug-Modus eine Assertion hinzu, dass der Wert wirklich in ein int passt, und fügt andernfalls nur eine Typumwandlung hinzu.
- statisch feststellen, dass der Wert kein int überschreiten sollte, es sei denn, es liegt ein Fehler im C-Code vor. Prüfen Sie, ob der Wert kleiner als INT_MAX ist, und lösen Sie andernfalls einen InternalError aus.
- andernfalls prüfen Sie, ob der Wert in ein int passt, und lösen Sie andernfalls einen ValueError aus.
Die gleiche Sorgfalt ist für tp_as_sequence-Slots erforderlich, zusätzlich ändern sich die Signaturen dieser Slots, und die Slots müssen explizit neu typumgewandelt werden (z. B. von intargfunc zu ssizeargfunc). Kompatibilität mit früheren Python-Versionen kann mit dem Test erreicht werden
#if PY_VERSION_HEX < 0x02050000 && !defined(PY_SSIZE_T_MIN)
typedef int Py_ssize_t;
#define PY_SSIZE_T_MAX INT_MAX
#define PY_SSIZE_T_MIN INT_MIN
#endif
und dann Py_ssize_t im restlichen Code verwenden. Für die tp_as_sequence-Slots können zusätzliche Typedefs erforderlich sein; alternativ durch Ersetzen von
PyObject* foo_item(struct MyType* obj, int index)
{
...
}
mit
PyObject* foo_item(PyObject* _obj, Py_ssize_t index)
{
struct MyType* obj = (struct MyType*)_obj;
...
}
wird es möglich, die Typumwandlung ganz wegzulassen; der Typ von foo_item sollte dann in allen Python-Versionen mit dem sq_item-Slot übereinstimmen.
Wenn das Modul erweitert werden soll, um Py_ssize_t-Indizes zu verwenden, sollten alle Verwendungen des Typs int überprüft werden, um festzustellen, ob sie in Py_ssize_t geändert werden sollten. Der Compiler hilft dabei, die Stellen zu finden, aber eine manuelle Überprüfung ist immer noch notwendig.
Besondere Sorgfalt ist für PyArg_ParseTuple-Aufrufe geboten: Sie müssen alle auf s#- und t#-Konverter überprüft werden, und PY_SSIZE_T_CLEAN muss vor der Einbindung von Python.h definiert werden, wenn die Aufrufe entsprechend aktualisiert wurden.
Fredrik Lundh hat einen Scanner geschrieben, der den Code eines C-Moduls auf die Verwendung von APIs prüft, deren Signatur sich geändert hat.
Diskussion
Warum nicht size_t
Ein erster Versuch, diese Funktion zu implementieren, versuchte, size_t zu verwenden. Es stellte sich schnell heraus, dass dies nicht funktionieren kann: Python verwendet an vielen Stellen negative Indizes (um das Zählen vom Ende anzuzeigen). Selbst an Stellen, an denen size_t verwendbar wäre, waren zu viele Umformulierungen von Code erforderlich, z. B. in Schleifen wie
for(index = length-1; index >= 0; index--)
Diese Schleife wird nie enden, wenn index von int zu size_t geändert wird.
Warum nicht Py_intptr_t
Konzeptionell sind Py_intptr_t und Py_ssize_t unterschiedliche Dinge: Py_intptr_t muss die gleiche Größe wie void* haben und Py_ssize_t die gleiche Größe wie size_t. Diese könnten sich unterscheiden, z. B. auf Maschinen, bei denen Zeiger ein Segment und einen Offset haben. Auf aktuellen Maschinen mit flachem Adressraum gibt es keinen Unterschied, sodass Py_intptr_t für alle praktischen Zwecke ebenfalls funktioniert hätte.
Bricht das viel Code?
Mit den vorgeschlagenen Änderungen ist der Codebruch relativ gering. Auf einem 32-Bit-System bricht kein Code, da Py_ssize_t nur ein Typedef für int ist.
Auf einem 64-Bit-System wird der Compiler an vielen Stellen warnen. Wenn diese Warnungen ignoriert werden, funktioniert der Code weiterhin, solange die Containergrößen 2**31 nicht überschreiten, d. h. er funktioniert fast so gut wie bisher. Es gibt zwei Ausnahmen von dieser Aussage: Wenn das Erweiterungsmodul das Sequenzprotokoll implementiert, muss es aktualisiert werden, oder die Aufrufkonventionen sind falsch. Die andere Ausnahme sind Stellen, an denen Py_ssize_t über einen Zeiger ausgegeben wird (und nicht als Rückgabewert); dies betrifft vor allem Codecs und Slice-Objekte.
Wenn die Konvertierung des Codes vorgenommen wird, kann derselbe Code weiterhin auf früheren Python-Versionen funktionieren.
Verbraucht das zu viel Speicher?
Man könnte denken, dass die Verwendung von Py_ssize_t in allen Tupeln, Strings, Listen usw. Platzverschwendung ist. Das ist jedoch nicht wahr: Auf einer 32-Bit-Maschine gibt es keine Änderung. Auf einer 64-Bit-Maschine ändert sich die Größe vieler Container nicht, z. B.
- in Listen und Tupeln folgt unmittelbar nach dem ob_size-Member ein Zeiger. Das bedeutet, dass der Compiler derzeit 4 Auffüllbytes einfügt; mit der Änderung werden diese Auffüllbytes Teil der Größe.
- In Strings folgt dem ob_size-Feld das Feld ob_shash. Dieses Feld ist vom Typ long, was auf den meisten 64-Bit-Systemen (außer Win64) ein 64-Bit-Typ ist, sodass der Compiler davor ebenfalls Auffüllung einfügt.
Offene Fragen
- Marc-Andre Lemburg merkte an, dass die vollständige Abwärtskompatibilität mit vorhandenem Quellcode erhalten bleiben sollte. Insbesondere Funktionen, die Py_ssize_t*-Ausgabeargumente haben, sollten weiterhin korrekt funktionieren, auch wenn die Aufrufer int*-Argumente übergeben.
Es ist unklar, welche Strategie verwendet werden könnte, um diese Anforderung zu erfüllen.
Urheberrecht
Dieses Dokument wurde gemeinfrei erklärt.
Quelle: https://github.com/python/peps/blob/main/peps/pep-0353.rst
Zuletzt geändert: 2025-02-01 08:59:27 GMT