PEP 611 – Die Eine-Million-Grenze
- Autor:
- Mark Shannon <mark at hotpy.org>
- Status:
- Zurückgezogen
- Typ:
- Standards Track
- Erstellt:
- 05-Dez-2019
- Post-History:
Zusammenfassung
Diese PR schlägt eine weiche Grenze von einer Million (1.000.000) und eine größere harte Grenze für verschiedene Aspekte von Python-Code und dessen Implementierung vor.
Die Python-Sprache legt keine Grenzen für viele ihrer Merkmale fest. Keine Grenze für diese Werte scheint die Freiheit des Programmierers zu erhöhen, zumindest oberflächlich, aber in der Praxis haben die CPython VM und andere Python-virtuelle Maschinen implizite Grenzen oder sind gezwungen anzunehmen, dass die Grenzen astronomisch sind, was teuer ist.
Diese PR listet eine Reihe von Merkmalen auf, die eine Grenze von einer Million haben sollen.
Für CPython wird die harte Grenze acht Millionen (8.000.000) betragen.
Motivation
Es gibt viele Werte, die in einer virtuellen Maschine dargestellt werden müssen. Wenn für diese Werte keine Grenze angegeben ist, muss die Darstellung entweder ineffizient sein oder anfällig für Überläufe. Die CPython-virtuelle Maschine stellt Werte wie Zeilennummern, Stack-Offsets und Instruktions-Offsets durch 32-Bit-Werte dar. Dies ist ineffizient und potenziell unsicher.
Es ist ineffizient, da tatsächliche Werte selten mehr als ein Dutzend Bits benötigen, um sie darzustellen.
Es ist unsicher, da bösartiger oder schlecht generierter Code dazu führen kann, dass Werte 232 überschreiten.
Zum Beispiel werden Zeilennummern intern durch 32-Bit-Werte dargestellt. Dies ist ineffizient, da Module fast nie mehr als ein paar tausend Zeilen überschreiten. Trotzdem ist es immer noch anfällig für Überläufe, da es für einen Angreifer einfach ist, ein Modul mit Milliarden von Zeilenumbrüchen zu erstellen.
Speicherzugriff ist in der Leistung moderner CPUs normalerweise ein begrenzender Faktor. Bessere Packung von Datenstrukturen verbessert die Lokalität und reduziert die Speicherbandbreite, bei einem moderaten Anstieg der ALU-Nutzung (für Schiebe- und Maskierungsoperationen). Die Möglichkeit, wichtige Werte sicher in 20 Bits zu speichern, würde Speicherersparnisse in mehreren Datenstrukturen ermöglichen, einschließlich, aber nicht beschränkt auf
- Frame-Objekte
- Objekt-Header
- Code-Objekte
Es gibt auch das Potenzial für ein effizienteres Instruktionsformat, das den Interpreter-Dispatch beschleunigt.
Ist dies ein lohnenswerter Kompromiss?
Der Nachteil jeder Form von Grenze ist, dass sie potenziell die Arbeit von jemandem erschweren kann, zum Beispiel kann es schwieriger sein, einen Code-Generator zu schreiben, der die Größe von Modulen auf eine Million Zeilen beschränkt. Die Meinung des Autors, der viele Code-Generatoren geschrieben hat, ist jedoch, dass eine solche Grenze höchstwahrscheinlich kein praktisches Problem darstellen wird.
Der Vorteil dieser Grenzen ist die Freiheit, die sie Implementierern von Laufzeitumgebungen, sei es CPython, PyPy oder eine andere Implementierung, gewährt, um die Leistung zu verbessern. Der Autor glaubt, dass der potenzielle Wert selbst einer Reduzierung der Kosten für die Ausführung von Python-Programmen um 0,1 % weltweit die Kosten für die Änderung einer Handvoll Code-Generatoren bei weitem übersteigen wird.
Begründung
Die Festlegung einer Grenze für Werte wie Zeilenanzahl in einem Modul und die Anzahl lokaler Variablen hat erhebliche Vorteile für die Implementierungsfreundlichkeit und die Effizienz virtueller Maschinen. Wenn die Grenze ausreichend groß ist, hat dies keine nachteiligen Auswirkungen auf die Benutzer der Sprache.
Durch die Auswahl einer festen, aber großen Grenze für diese Werte ist es möglich, sowohl Sicherheit als auch Effizienz zu gewährleisten, ohne menschliche Programmierer zu beeinträchtigen und nur sehr seltene Probleme für Code-Generatoren zu verursachen.
Eine Million
Der Wert "eine Million" ist sehr leicht zu merken.
Die Eine-Million-Grenze ist hauptsächlich eine Grenze für von Menschen erstellten Code, nicht für Laufzeitgrößen.
Eine Million Zeilen in einem einzigen Modul ist eine lächerliche Konzentration von Code; die gesamte Python-Standardbibliothek umfasst etwa 2/3 Million Zeilen, verteilt auf 1600 Dateien.
Die Java Virtual Machine (JVM) [1] legt für viele Programmelemente, die hier behandelt werden, eine Grenze von 216-1 (65535) fest. Diese Grenze ermöglicht es, begrenzte Werte in 16 Bit unterzubringen, was eine sehr effiziente Maschinenrepräsentation ist. Diese Grenze wird jedoch in der Praxis von Code-Generatoren recht leicht überschritten, und der Autor ist sich bewusst, dass existierender Python-Code bereits mehr als 216 Zeilen Code überschreitet.
Die harte Grenze von acht Millionen passt in 23 Bit, was, obwohl nicht so praktisch für die Maschinenrepräsentation, immer noch relativ kompakt ist. Eine Grenze von acht Millionen ist klein genug für Effizienzvorteile (nur 23 Bit), aber groß genug, um Benutzer nicht zu beeinträchtigen (niemand hat jemals ein so großes Modul geschrieben).
Während es möglich ist, dass generierter Code die Grenze überschreitet, ist es für einen Code-Generator einfach, seine Ausgabe anzupassen. Der Autor ist bei der Generierung von Java-Code mindestens zweimal auf die 64K-Grenze der JVM gestoßen. Die Workarounds waren relativ unkompliziert und wären bei einer Grenze von einer Million Bytecodes oder Codezeilen nicht notwendig gewesen.
Wo nötig, kann die weiche Grenze für Programme erhöht werden, die die Eine-Million-Grenze überschreiten.
Eine weiche Grenze von einer Million bietet eine Warnung vor problematischem Code, ohne einen Fehler zu verursachen und eine sofortige Korrektur zu erzwingen. Sie ermöglicht auch dynamischen Optimierern die Verwendung kompakterer Formate ohne Inline-Checks.
Spezifikation
Diese PR schlägt vor, dass die folgenden Sprachmerkmale und Laufzeitwerte eine weiche Grenze von einer Million haben.
- Die Anzahl der Quellcodezeilen in einem Modul
- Die Anzahl der Bytecode-Instruktionen in einem Code-Objekt.
- Die Summe der lokalen Variablen und der Stack-Nutzung für ein Code-Objekt.
- Die Anzahl der Klassen in einem laufenden Interpreter.
- Die Rekursionstiefe von Python-Code.
Es ist wahrscheinlich, dass Speicherbeschränkungen ein begrenzender Faktor sein werden, bevor die Anzahl der Klassen eine Million erreicht.
Rekursionstiefe
Die Rekursionstiefenbegrenzung gilt nur für reinen Python-Code. In einer Fremdsprache wie C geschriebener Code kann den Hardware-Stack beanspruchen und ist daher auf eine Rekursionstiefe von einigen tausend begrenzt. Es wird erwartet, dass Implementierungen eine Ausnahme auslösen, wenn der Hardware-Stack seine Grenze erreicht. Für Code, der Python- und C-Aufrufe mischt, wird höchstwahrscheinlich zuerst die Hardware-Grenze gelten. Die Größe der Hardware-Rekursion kann zur Laufzeit variieren und ist nicht sichtbar.
Weiche und harte Grenzen
Implementierungen sollten eine Warnung ausgeben, wenn eine weiche Grenze überschritten wird, es sei denn, die harte Grenze hat den gleichen Wert wie die weiche Grenze. Wenn eine harte Grenze überschritten wird, sollte eine Ausnahme ausgelöst werden.
Je nach Implementierung können unterschiedliche harte Grenzen gelten. In einigen Fällen kann die harte Grenze unter der weichen Grenze liegen. Viele MicroPython-Ports können zum Beispiel wahrscheinlich keine so großen Grenzen unterstützen.
Inspektion und Modifizierung der Grenzen
Eine oder mehrere Funktionen werden im Modul sys bereitgestellt, um die weichen Grenzen zur Laufzeit zu inspizieren oder zu modifizieren, aber die Grenzen dürfen nicht über die harte Grenze hinaus erhöht werden.
Abgeleitete Grenzen
Diese Grenzen sind nicht Teil der Spezifikation, aber eine Grenze von weniger als einer Million kann aus der Grenze für die Anzahl der Bytecode-Instruktionen in einem Code-Objekt abgeleitet werden. Da nicht genügend Instruktionen vorhanden wären, um mehr als eine Million Konstanten zu laden oder mehr als eine Million Namen zu verwenden.
- Die Anzahl der unterschiedlichen Namen in einem Code-Objekt.
- Die Anzahl der Konstanten in einem Code-Objekt.
Die Vorteile für CPython, diese Grenzen festzulegen
Zeilenanzahl in einem Modul und Beschränkungen von Codeobjekten.
Beim Kompilieren von Quellcode zu Bytecode oder beim Modifizieren von Bytecode für Profiling oder Debugging ist ein Zwischenformat erforderlich. Durch die Beschränkung von Operanden auf 23 Bit können Instruktionen in einem kompakten 64-Bit-Format dargestellt werden, was sehr schnelle Durchläufe über die Instruktionssequenz ermöglicht.
23-Bit-Operanden (24 Bit für relative Sprünge) ermöglichen es, Instruktionen in 32 Bit unterzubringen, ohne zusätzliche EXTENDED_ARG-Instruktionen zu benötigen. Dies verbessert die Dispatch-Geschwindigkeit, da der Operand streng lokal zur Instruktion ist. Es ist unklar, ob dies die Leistung verbessern würde, es ist lediglich ein Beispiel dafür, was möglich ist.
Der Vorteil der Beschränkung der Anzahl der Zeilen in einem Modul ist hauptsächlich die implizite Begrenzung der Bytecodes. Für Implementierungen ist es wichtiger, dass die Anzahl der Instruktionen pro Code-Objekt und nicht die Anzahl der Zeilen pro Modul auf eine Million begrenzt ist, aber es ist viel einfacher, eine Ein-Million-Zeilen-Grenze zu erklären. Eine einheitliche Grenze von einer Million ist einfach leichter zu merken. Es ist höchstwahrscheinlich, wenn auch nicht garantiert, dass die Zeilengrenze zuerst erreicht wird und somit eine einfachere Fehlermeldung für den Entwickler liefert.
Gesamtzahl der Klassen in einem laufenden Interpreter
Diese Grenze hat das Potenzial, die Größe von Objekt-Headern erheblich zu reduzieren.
Derzeit haben Objekte einen Zwei-Wort-Header für Objekte ohne Referenzen (int, float, str, etc.) oder einen Vier-Wort-Header für Objekte mit Referenzen. Durch die Reduzierung der maximalen Anzahl von Klassen kann der Speicherplatz für die Klassenreferenz von 64 Bit auf weniger als 32 Bit reduziert werden, was einen wesentlich kompakteren Header ermöglicht.
Zum Beispiel könnte ein superkompaktes Header-Format so aussehen
struct header {
uint32_t gc_flags:6; /* Needs finalisation, might be part of a cycle, etc. */
uint32_t class_id:26; /* Can be efficiently mapped to address by ensuring suitable alignment of classes */
uint32_t refcount; /* Limited memory or saturating */
}
Dieses Format würde die Größe eines Python-Objekts ohne Slots auf einer 64-Bit-Maschine von 40 auf 16 Bytes reduzieren.
Beachten Sie, dass es zwei Möglichkeiten gibt, einen 32-Bit-Refcount auf einer 64-Bit-Maschine zu verwenden. Eine besteht darin, jeden Sub-Interpreter auf 32 GB Speicher zu beschränken. Die andere ist die Verwendung eines sättigenden Referenzzählers, der etwas langsamer wäre, aber eine unbegrenzte Speicherzuweisung ermöglichen würde.
Durchsetzung
Python-Implementierungen sind nicht verpflichtet, die Grenzen durchzusetzen. Wenn eine Grenze jedoch ohne Leistungseinbußen durchgesetzt werden kann, sollte sie durchgesetzt werden.
Es wird erwartet, dass CPython die Grenzen wie folgt durchsetzt
- Die Anzahl der Quellcodezeilen in einem Modul: Version 3.9 und höher.
- Die Anzahl der Bytecode-Instruktionen in einem Code-Objekt: 3.9 und höher.
- Die Summe der lokalen Variablen und der Stack-Nutzung für ein Code-Objekt: 3.9 und höher.
- Die Anzahl der Klassen in einem laufenden Interpreter: wahrscheinlich ab 3.10, vielleicht eine Warnung in 3.9.
Harte Grenzen in CPython
CPython wird eine harte Grenze für alle oben genannten Werte durchsetzen. Der Wert der harten Grenze wird 8 Millionen betragen.
Es ist theoretisch möglich, dass maschinell generierter Code eine oder mehrere der oben genannten Grenzen überschreitet. Der Autor glaubt, dass dies äußerst unwahrscheinlich und leicht durch die Anpassung der Ausgabephase des Code-Generators zu beheben ist.
Wir möchten die Vorteile der oben genannten Grenzen für die Leistung so bald wie möglich nutzen. Zu diesem Zweck wird CPython ab Version 3.9 mit der Anwendung von Grenzen beginnen. Um den Übergang zu erleichtern und Unterbrechungen zu minimieren, werden die anfänglichen Grenzen 16 Millionen betragen und in einer späteren Version auf 8 Millionen reduziert.
Abwärtskompatibilität
Die tatsächlich von CPython durchgesetzten harten Grenzen werden sein
| Version | Harte Grenze |
|---|---|
| 3.9 | 16 Millionen |
| Ab 3.10 | 8 Millionen |
Angesichts der Seltenheit von Code-Generatoren, die die Eine-Million-Grenze überschreiten würden, und der Umgebungen, in denen sie typischerweise verwendet werden, erscheint es angemessen, ab 3.9 Warnungen auszugeben, wenn eine begrenzte Menge eine Million überschreitet.
Historisch wurde die Rekursionsgrenze auf 1000 gesetzt. Um Code, der implizit auf einem kleinen Wert basiert, nicht zu brechen, wird die weiche Rekursionsgrenze schrittweise wie folgt erhöht
| Version | Weiche Grenze |
|---|---|
| 3.9 | 4 000 |
| 3.10 | 16 000 |
| 3.11 | 64 000 |
| 3.12 | 125 000 |
| 3.13 | 1 Million |
Die harte Grenze wird sofort auf 8 Millionen gesetzt.
Andere Implementierungen
Implementierungen von Python, die nicht CPython sind, haben unterschiedliche Zwecke, daher können unterschiedliche Grenzen angemessen sein. Dies ist akzeptabel, vorausgesetzt, die Grenzen sind klar dokumentiert.
Allzweck-Implementierungen
Allzweck-Implementierungen wie PyPy sollten die Eine-Million-Grenze verwenden. Wenn maximale Kompatibilität ein Ziel ist, sollten sie auch dem Verhalten von CPython für 3.9 bis 3.11 folgen.
Spezielle Implementierungen
Spezielle Implementierungen können niedrigere Grenzen verwenden, solange sie klar dokumentiert sind. Eine für eingebettete Systeme konzipierte Implementierung, wie z. B. MicroPython, könnte beispielsweise Grenzen von nur wenigen Tausend festlegen.
Sicherheitsimplikationen
Minimal. Dies reduziert die Angriffsfläche jeder Python-virtuellen Maschine geringfügig.
Referenzimplementierung
Noch keine. Dies wird in CPython implementiert, sobald die PEP akzeptiert wurde.
Abgelehnte Ideen
Die Möglichkeit, harte Grenzen zur Kompilierungszeit nach oben zu modifizieren, wurde von Tal Einat vorgeschlagen. Dies wird abgelehnt, da die aktuellen Grenzen von 232 kein Problem darstellten und die praktischen Vorteile, Grenzen zwischen 220 und 232 zu erlauben, im Vergleich zur zusätzlichen Code-Komplexität, die für die Unterstützung einer solchen Funktion erforderlich ist, gering erscheinen.
Offene Fragen
Noch keine.
Referenzen
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-0611.rst
Zuletzt geändert: 2025-02-01 08:55:40 GMT