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

Python Enhancement Proposals

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:


Inhaltsverzeichnis

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.


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

Zuletzt geändert: 2025-02-01 08:59:27 GMT