PEP 683 – Unsterbliche Objekte, Verwendung eines festen Referenzzählers
- Autor:
- Eric Snow <ericsnowcurrently at gmail.com>, Eddie Elizondo <eduardo.elizondorueda at gmail.com>
- Discussions-To:
- Discourse thread
- Status:
- Final
- Typ:
- Standards Track
- Erstellt:
- 10. Feb. 2022
- Python-Version:
- 3.12
- Post-History:
- 16. Feb. 2022, 19. Feb. 2022, 28. Feb. 2022, 12. Aug. 2022
- Resolution:
- Discourse-Nachricht
Inhaltsverzeichnis
- Bedingungen für die Annahme von PEPs
- Zusammenfassung
- Motivation
- Begründung
- Auswirkungen
- Spezifikation
- Details des öffentlichen Referenzzählers
- Beschränkungen
- Unsterbliche veränderliche Objekte
- Implizit unsterbliche Objekte
- Objekte ent-unsterblich machen
- _Py_IMMORTAL_REFCNT
- Betroffene API
- Unsterbliche globale Objekte
- Objektreinigung
- Abmilderung von Leistungsregressionen
- Lösungen für versehentliche Ent-Unsterblichmachung
- Dokumentation
- Referenzimplementierung
- Offene Fragen
- Referenzen
- Urheberrecht
Bedingungen für die Annahme von PEPs
Die PEP wurde mit Auflagen angenommen
- der Hauptvorschlag in Lösungen für versehentliche Ent-Unsterblichmachung (Zurücksetzen des unsterblichen Referenzzählers in
tp_dealloc()) wurde angewendet - Typen ohne dies wurden nicht unsterblich gemacht (im CPython-Code)
- die PEP wurde mit endgültigen Benchmark-Ergebnissen aktualisiert, sobald die Implementierung abgeschlossen ist (bestätigt, dass die Änderung lohnenswert ist)
Zusammenfassung
Derzeit unterhält die CPython-Laufzeit eine geringe Menge an veränderlichem Zustand im zugeordneten Speicher jedes Objekts. Aus diesem Grund sind ansonsten unveränderliche Objekte tatsächlich veränderlich. Dies kann sich stark negativ auf die CPU- und Speicherleistung auswirken, insbesondere bei Ansätzen zur Erhöhung der Skalierbarkeit von Python.
Dieser Vorschlag schreibt vor, dass CPython intern die Markierung eines Objekts als eines unterstützen wird, für das sich der Laufzeitzustand nicht mehr ändert. Folglich wird der Referenzzähler eines solchen Objekts niemals 0 erreichen, und somit wird das Objekt niemals bereinigt (es sei denn, die Laufzeit weiß, dass dies sicher ist, wie z. B. während der Laufzeitfinalisierung). Wir nennen diese Objekte "unsterblich". (Normalerweise werden nur relativ wenige interne Objekte jemals unsterblich sein.) Die grundlegende Verbesserung hier ist, dass ein Objekt jetzt wirklich unveränderlich sein kann.
Umfang
Objektunsterblichkeit soll eine reine interne Funktion sein. Daher beinhaltet dieser Vorschlag keine Änderungen an der öffentlichen API oder dem Verhalten (mit einer Ausnahme). Wie üblich können wir immer noch einige private (aber öffentlich zugängliche) APIs hinzufügen, um beispielsweise ein Objekt unsterblich zu machen oder festzustellen, ob es unsterblich ist. Jeder Versuch, diese Funktion für Benutzer zugänglich zu machen, müsste separat vorgeschlagen werden.
Es gibt eine Ausnahme von "keine Verhaltensänderung": die Referenzzählungssemantik für unsterbliche Objekte unterscheidet sich in einigen Fällen von den Erwartungen der Benutzer. Diese Ausnahme und ihre Lösung werden unten diskutiert.
Der größte Teil dieser PEP konzentriert sich auf eine interne Implementierung, die die oben genannte Anforderung erfüllt. Diese Implementierungsdetails sind jedoch nicht streng vorschreibend gemeint. Stattdessen werden sie zumindest dazu beitragen, die für die Anforderung erforderlichen technischen Überlegungen zu veranschaulichen. Die tatsächliche Implementierung kann geringfügig abweichen, solange sie die unten skizzierten Einschränkungen erfüllt. Darüber hinaus hängt die Akzeptanz jedes spezifischen unten beschriebenen Implementierungsdetails nicht vom Status dieser PEP ab, es sei denn, dies ist ausdrücklich angegeben.
Zum Beispiel die spezifischen Details von
- wie etwas als unsterblich markiert wird
- wie etwas als unsterblich erkannt wird
- welcher Teil von funktional unsterblichen Objekten als unsterblich markiert wird
- welche speicherverwaltenden Aktivitäten für unsterbliche Objekte übersprungen oder modifiziert werden
sind nicht nur CPython-spezifisch, sondern auch private Implementierungsdetails, die sich voraussichtlich in zukünftigen Versionen ändern werden.
Zusammenfassung der Implementierung
Hier ist ein Überblick über die Implementierung
Wenn der Referenzzähler eines Objekts mit einem sehr spezifischen Wert (unten definiert) übereinstimmt, wird dieses Objekt als unsterblich behandelt. Die CPython C-API und Laufzeit ändern den Referenzzähler (oder andere Laufzeitzustände) eines unsterblichen Objekts nicht. Die Laufzeit ist nun explizit dafür verantwortlich, alle unsterblichen Objekte während der Finalisierung freizugeben, es sei denn, sie sind statisch zugewiesen. (Siehe Objektreinigung unten.)
Neben der Änderung der Referenzzählungssemantik gibt es eine weitere mögliche negative Auswirkung zu beachten. Die Schwelle für eine "akzeptable" Leistungseinbuße für unsterbliche Objekte beträgt 2 % (Konsens auf dem Language Summit 2022). Eine naive Implementierung des unten beschriebenen Ansatzes macht CPython etwa 4 % langsamer. Die Implementierung ist jedoch nach Anwendung bekannter Abhilfemaßnahmen ~leistungsneutral~.
TODO: Leistungsauswirkungen für den neuesten Branch aktualisieren (sowohl für GCC als auch für Clang).
Motivation
Wie oben erwähnt, hat derzeit jedes Objekt effektiv einen veränderlichen Zustand. Dazu gehören auch "unveränderliche" Objekte wie str-Instanzen. Das liegt daran, dass der Referenzzähler jedes Objekts während der Laufzeit häufig geändert wird, wenn das Objekt verwendet wird. Dies ist besonders wichtig für eine Reihe von häufig verwendeten globalen (eingebauten) Objekten, z. B. None. Solche Objekte werden sowohl im Python-Code als auch intern häufig verwendet. Das summiert sich zu einem konstant hohen Volumen von Referenzzähleränderungen.
Die effektive Veränderlichkeit aller Python-Objekte hat konkrete Auswirkungen auf Teile der Python-Community, z. B. auf Projekte, die auf Skalierbarkeit abzielen, wie Instagram, oder auf Bemühungen, das GIL pro Interpreter zu machen. Im Folgenden beschreiben wir mehrere Möglichkeiten, wie sich die Änderung des Referenzzählers negativ auf solche Projekte auswirkt. Nichts davon würde für Objekte passieren, die wirklich unveränderlich sind.
Reduzierung von CPU-Cache-Invalidierungen
Jede Änderung eines Referenzzählers führt zur Ungültigkeit der entsprechenden CPU-Cache-Zeile. Dies hat eine Reihe von Auswirkungen.
Zum einen muss der Schreibvorgang auf andere Cache-Ebenen und in den Hauptspeicher propagiert werden. Dies hat geringe Auswirkungen auf alle Python-Programme. Unsterbliche Objekte würden in dieser Hinsicht eine leichte Erleichterung bieten.
Darüber hinaus zahlen Multi-Core-Anwendungen einen Preis. Wenn zwei Threads (gleichzeitig auf verschiedenen Kernen ausgeführt) mit demselben Objekt interagieren (z. B. None), ungültigen sie sich gegenseitig mit jedem incref und decref die Caches. Dies gilt selbst für ansonsten unveränderliche Objekte wie True, 0 und str-Instanzen. CPythons GIL hilft, diesen Effekt zu reduzieren, da nur ein Thread gleichzeitig ausgeführt wird, aber er eliminiert die Strafe nicht vollständig.
Vermeidung von Datenrennen (Data Races)
Apropos Multi-Core: Wir erwägen, das GIL zu einem Pro-Interpreter-Lock zu machen, was echte Multi-Core-Parallelität ermöglichen würde. Unter anderem schützt das GIL derzeit vor Rennen zwischen mehreren gleichzeitigen Threads, die denselben Objekt inkrementieren oder dekrementieren können. Ohne ein geteiltes GIL könnten zwei laufende Interpreter keine Objekte sicher teilen, nicht einmal ansonsten unveränderliche wie None.
Das bedeutet, um ein Pro-Interpreter-GIL zu haben, muss jeder Interpreter seine eigene Kopie von *jedem* Objekt haben. Dazu gehören auch die Singletons und statischen Typen. Wir haben eine praktikable Strategie dafür, aber sie erfordert einen erheblichen zusätzlichen Aufwand und zusätzliche Komplexität.
Die Alternative besteht darin, sicherzustellen, dass alle gemeinsam genutzten Objekte wirklich unveränderlich sind. Es gäbe keine Rennen, da es keine Modifikation gäbe. Dies ist etwas, das die hier vorgeschlagene Unsterblichkeit für ansonsten unveränderliche Objekte ermöglichen würde. Mit unsterblichen Objekten wird die Unterstützung für ein Pro-Interpreter-GIL viel einfacher.
Vermeidung von Copy-on-Write
Für einige Anwendungen ist es sinnvoll, die Anwendung in einen gewünschten Anfangszustand zu versetzen und dann den Prozess für jeden Worker zu forken. Dies kann zu einer erheblichen Leistungssteigerung führen, insbesondere beim Speicherverbrauch. Mehrere Enterprise-Python-Benutzer (z. B. Instagram, YouTube) haben dies genutzt. Die oben genannte Referenzzählungssemantik reduziert jedoch die Vorteile drastisch und hat zu einigen suboptimalen Workarounds geführt.
Beachten Sie auch, dass "fork" nicht der einzige Mechanismus des Betriebssystems ist, der Copy-on-Write-Semantik verwendet. Ein weiteres Beispiel ist mmap. Jedes solche Dienstprogramm profitiert potenziell von weniger Copy-on-Writes, wenn unsterbliche Objekte beteiligt sind, verglichen mit der Verwendung von nur "sterblichen" Objekten.
Begründung
Die vorgeschlagene Lösung ist so offensichtlich, dass beide Autoren dieses Vorschlags unabhängig voneinander zur gleichen Schlussfolgerung (und mehr oder weniger zur gleichen Implementierung) gelangt sind. Das Pyston-Projekt verwendet einen ähnlichen Ansatz. Es wurden auch andere Designs in Betracht gezogen. Mehrere Möglichkeiten wurden auch in den letzten Jahren auf python-dev diskutiert.
Alternativen umfassen
- verwende ein hohes Bit, um "unsterblich" zu markieren, aber ändere
Py_INCREF()nicht - füge ein explizites Flag zu Objekten hinzu
- implementiere über den Typ (
tp_dealloc()ist ein No-Op) - verfolge über das Typobjekt des Objekts
- verfolge mit einer separaten Tabelle
Jede der oben genannten Optionen macht Objekte unsterblich, aber keine davon adressiert die oben beschriebenen Leistungseinbußen durch Referenzzähleränderungen.
Im Falle eines Pro-Interpreter-GIL ist die einzig realistische Alternative, alle globalen Objekte in PyInterpreterState zu verschieben und eine oder mehrere Lookup-Funktionen hinzuzufügen, um darauf zuzugreifen. Dann müssten wir einige Hacks zur C-API hinzufügen, um die Kompatibilität für die vielen dort exponierten Objekte zu erhalten. Die Geschichte ist mit unsterblichen Objekten viel, viel einfacher.
Auswirkungen
Vorteile
Insbesondere die im obigen Beispielen beschriebenen Fälle profitieren stark von unsterblichen Objekten. Projekte, die Pre-Forking nutzen, können ihre Workarounds einstellen. Für das Pro-Interpreter-GIL-Projekt vereinfachen unsterbliche Objekte die Lösung für bestehende statische Typen erheblich, ebenso wie für Objekte, die von der öffentlichen C-API bereitgestellt werden.
Im Allgemeinen ermöglicht eine starke Unveränderlichkeitsgarantie für Objekte, dass Python-Anwendungen besser skalieren, insbesondere in Multi-Prozess-Bereitstellungen. Denn sie können dann Multi-Core-Parallelität nutzen, ohne einen so erheblichen Kompromiss bei der Speichernutzung einzugehen, wie sie es derzeit tun. Die gerade beschriebenen Fälle sowie die oben in Motivation beschriebenen spiegeln diese Verbesserung wider.
Performance
Eine naive Implementierung zeigt eine Verlangsamung um 2 % (3 % mit MSVC). Wir haben mit einer Handvoll grundlegender Abhilfemaßnahmen eine Rückkehr zu ~Leistungsneutralität~ gezeigt. Siehe den Abschnitt Abhilfemaßnahmen unten.
Auf der positiven Seite sparen unsterbliche Objekte erheblich Speicher, wenn sie mit einem Pre-Fork-Modell verwendet werden. Außerdem bieten unsterbliche Objekte Möglichkeiten zur Spezialisierung in der Auswertungs-Schleife, die die Leistung verbessern würden.
Abwärtskompatibilität
Idealerweise wäre diese reine interne Funktion vollständig kompatibel. Sie beinhaltet jedoch in einigen Fällen eine Änderung der Referenzzählungssemantik. Nur unsterbliche Objekte sind betroffen, aber dazu gehören häufig verwendete Objekte wie None, True und False.
Insbesondere, wenn ein unsterbliches Objekt beteiligt ist
- Code, der den Referenzzähler inspiziert, sieht einen wirklich, wirklich großen Wert
- das neue Noop-Verhalten kann Code brechen, der
- hängt speziell vom Referenzzähler ab, um immer zu inkrementieren oder zu dekrementieren (oder einen bestimmten Wert von
Py_SET_REFCNT()zu haben) - verlässt sich auf einen spezifischen Referenzzählerwert, außer 0 oder 1
- manipuliert den Referenzzähler direkt, um dort zusätzliche Informationen zu speichern
- hängt speziell vom Referenzzähler ab, um immer zu inkrementieren oder zu dekrementieren (oder einen bestimmten Wert von
- in 32-Bit-Erweiterungen vor 3.12 Stable ABI können Objekte aufgrund von versehentlicher Unsterblichkeit lecken
- solche Erweiterungen können aufgrund von versehentlicher Ent-Unsterblichmachung abstürzen
Auch hier gilt: Die Verhaltensänderungen gelten nur für unsterbliche Objekte, nicht für die überwiegende Mehrheit der Objekte, die ein Benutzer verwendet. Darüber hinaus können Benutzer ein Objekt nicht als unsterblich markieren, sodass keine vom Benutzer erstellten Objekte dieses geänderte Verhalten aufweisen werden. Benutzer, die sich auf irgendein geändertes Verhalten für globale (eingebaute) Objekte verlassen, haben bereits ein Problem. Die Gesamtauswirkungen sollten daher gering sein.
Beachten Sie auch, dass Code, der auf Referenzlecks prüft, weiterhin gut funktionieren sollte, es sei denn, er prüft auf fest codierte kleine Werte in Bezug auf ein unsterbliches Objekt. Die von Pyston bemerkten Probleme sollten hier nicht zutreffen, da wir den Referenzzähler nicht ändern.
Siehe Details des öffentlichen Referenzzählers unten für weitere Diskussionen.
Versehentliche Unsterblichkeit
Hypothetisch könnte ein nicht-unsterbliches Objekt so oft inkrementiert werden, dass es den magischen Wert erreicht, der für die Unsterblichkeit erforderlich ist. Das bedeutet, es würde nie wieder auf 0 dekrementiert, und es würde versehentlich lecken (nie bereinigt werden).
Bei 64-Bit-Referenzzählern ist dieses versehentliche Szenario so unwahrscheinlich, dass wir uns keine Sorgen machen müssen. Selbst wenn es absichtlich durch die Verwendung von Py_INCREF() in einer engen Schleife geschieht und jede Iteration nur 1 CPU-Zyklus dauert, würde es 2^60 Zyklen dauern (wenn das unsterbliche Bit 2^60 wäre). Bei schnellen 5 GHz würde dies immer noch fast 250.000.000 Sekunden dauern (über 2.500 Tage)!
Beachten Sie auch, dass es doppelt unwahrscheinlich ist, ein Problem zu sein, da es erst relevant wäre, wenn der Referenzzähler wieder auf 0 gefallen wäre und das Objekt bereinigt worden wäre. Jedes Objekt, das diesen magischen "unsterblichen" Referenzzählerwert erreicht, müsste also noch so oft dekrementiert werden, bevor die Verhaltensänderung bemerkt würde.
Auch hier ist der einzige realistische Weg, wie der magische Referenzzähler erreicht (und dann umgekehrt) werden könnte, wenn er absichtlich geschieht. (Natürlich könnte dasselbe effizient mit Py_SET_REFCNT() erreicht werden, obwohl das noch weniger ein Unfall wäre.) Zu diesem Zeitpunkt betrachten wir dies nicht als Problem dieses Vorschlags.
Auf Builds mit viel kleineren maximalen Referenzzählern, wie z. B. 32-Bit-Plattformen, sind die Konsequenzen nicht so offensichtlich. Nehmen wir an, der magische Referenzzähler wäre 2^30. Mit den gleichen Spezifikationen wie oben würde es ungefähr 4 Sekunden dauern, ein Objekt versehentlich unsterblich zu machen. Unter angemessenen Bedingungen ist es immer noch höchst unwahrscheinlich, dass ein Objekt versehentlich unsterblich gemacht wird. Es müsste folgende Kriterien erfüllen:
- Ziel auf ein nicht-unsterbliches Objekt (also keines der häufig verwendeten Builtins)
- die Erweiterung inkrementiert, ohne entsprechend zu dekrementieren (z. B. Rückgabe aus einer Funktion oder Methode)
- kein anderer Code dekrementiert das Objekt in der Zwischenzeit
Selbst bei einer viel geringeren Rate würde es nicht lange dauern, bis versehentliche Unsterblichkeit erreicht ist (auf 32-Bit). Allerdings müsste es dann durch die gleiche Anzahl von (jetzt noopenden) Dekrementierungen laufen, bevor dieses eine Objekt effektiv leckt. Dies ist höchst unwahrscheinlich, insbesondere da die Berechnungen keine Dekrementierungen annehmen.
Darüber hinaus ist dies nicht wesentlich anders als wie solche 32-Bit-Erweiterungen bereits ein Objekt über 2^31 inkrementieren und den Referenzzähler negativ machen können. Wenn das ein tatsächliches Problem wäre, hätten wir davon gehört.
In allen oben genannten Fällen betrachtet der Vorschlag versehentliche Unsterblichkeit nicht als Problem.
Stabile ABI
Der in dieser PEP beschriebene Implementierungsansatz ist mit Erweiterungen kompatibel, die zur Stable ABI kompiliert wurden (mit Ausnahme von versehentliche Unsterblichkeit und versehentliche Ent-Unsterblichmachung). Aufgrund der Natur der Stable ABI verwenden solche Erweiterungen leider Versionen von Py_INCREF() usw., die direkt das ob_refcnt-Feld des Objekts ändern. Dies wird alle Leistungsvorteile unsterblicher Objekte zunichte machen.
Wir stellen jedoch sicher, dass unsterbliche Objekte in dieser Situation (meistens) unsterblich bleiben. Wir setzen den anfänglichen Referenzzähler von unsterblichen Objekten auf einen Wert, für den wir das Objekt als unsterblich identifizieren können und der dies auch weiterhin tut, selbst wenn der Referenzzähler von einer Erweiterung geändert wird. (Nehmen wir zum Beispiel an, wir würden eines der oberen Referenzzähler-Bits verwenden, um anzuzeigen, dass ein Objekt unsterblich ist. Wir würden den anfänglichen Referenzzähler auf einen höheren Wert setzen, der immer noch mit dem Bit übereinstimmt, z. B. auf halbem Weg zum nächsten Bit. Siehe _Py_IMMORTAL_REFCNT.) Im schlimmsten Fall erfahren Objekte in dieser Situation die in der Sektion Motivation beschriebenen Auswirkungen. Selbst dann ist die Gesamtauswirkung wahrscheinlich nicht signifikant.
Versehentliche Ent-Unsterblichmachung
32-Bit-Builds älterer Stable-ABI-Erweiterungen können versehentliche Unsterblichkeit auf die nächste Stufe heben.
Hypothetisch könnte eine solche Erweiterung ein Objekt auf das nächsthöhere Bit über dem magischen Referenzzählerwert inkrementieren. Wenn zum Beispiel der magische Wert 2^30 wäre und der anfängliche unsterbliche Referenzzähler somit 2^30 + 2^29 wäre, würde es 2^29 Inkrementierungen durch die Erweiterung dauern, um einen Wert von 2^31 zu erreichen und das Objekt damit nicht-unsterblich zu machen. (Natürlich würde ein so hoher Referenzzähler wahrscheinlich ohnehin einen Absturz verursachen, unabhängig von unsterblichen Objekten.)
Der problematischere Fall ist, wenn eine solche 32-Bit-Stable-ABI-Erweiterung verrückt wird und ein bereits unsterbliches Objekt dekrementiert. Weiter im obigen Beispiel würde es 2^29 asymmetrische Dekrementierungen dauern, um unter den magischen unsterblichen Referenzzählerwert zu fallen. Ein Objekt wie None könnte also sterblich gemacht und dekrementiert werden. Das wäre immer noch kein Problem, bis die Dekrementierungen bei diesem Objekt irgendwie weitergehen und es 0 erreichen. Bei statisch zugewiesenen unsterblichen Objekten wie None würde die Erweiterung den Prozess abstürzen lassen, wenn sie versucht, das Objekt freizugeben. Bei allen anderen unsterblichen Objekten könnte die Freigabe in Ordnung sein. Es könnte jedoch Laufzeitcode geben, der erwartet, dass das ehemals unsterbliche Objekt für immer existiert. Dieser Code würde wahrscheinlich abstürzen.
Auch hier ist die Wahrscheinlichkeit, dass dies geschieht, extrem gering, selbst bei 32-Bit-Builds. Es würde etwa eine Milliarde Dekrementierungen auf diesem einen Objekt erfordern, ohne eine entsprechende Inkrementierung. Das wahrscheinlichste Szenario ist Folgendes:
Eine "neue" Referenz auf None wird von vielen Funktionen und Methoden zurückgegeben. Im Gegensatz zu nicht-unsterblichen Objekten wird die Laufzeit ab 3.12 None im Grunde nie inkrementieren, bevor sie es an die Erweiterung übergibt. Die Erweiterung wird es jedoch dekrementieren, wenn sie fertig ist (es sei denn, sie gibt es zurück). Jedes Mal, wenn dieser Austausch mit dem einen Objekt stattfindet, kommen wir dem Absturz einen Schritt näher.
Wie realistisch ist es, dass eine Form dieses Austauschs (mit einem einzigen Objekt) eine Milliarde Mal im Lebenszyklus eines Python-Prozesses auf 32-Bit stattfindet? Wenn es ein Problem ist, wie könnte es behoben werden?
Wie realistisch ist es? Die Antwort ist derzeit nicht klar. Die Abhilfe ist jedoch so einfach, dass wir sicher davon ausgehen können, dass es kein Problem wäre.
Wir betrachten mögliche Lösungen später.
Alternative Python-Implementierungen
Dieser Vorschlag ist CPython-spezifisch. Er bezieht sich jedoch auf das Verhalten der C-API, was andere Python-Implementierungen betreffen kann. Folglich gilt die Auswirkung geänderter Verhaltensweisen, die in Abwärtskompatibilität beschrieben sind, auch hier (z. B. wenn eine andere Implementierung eng an spezifische Referenzzählerwerte gekoppelt ist, außer 0, oder an die genaue Art und Weise, wie sich Referenzzähler ändern, dann können sie betroffen sein).
Sicherheitsimplikationen
Dieses Feature hat keine bekannten Auswirkungen auf die Sicherheit.
Wartbarkeit
Dies ist keine komplexe Funktion, daher sollte sie für Maintainer nicht viel zusätzlichen Aufwand bedeuten. Die grundlegende Implementierung berührt nicht viel Code, daher sollte sie wenig Auswirkungen auf die Wartbarkeit haben. Es mag zusätzliche Komplexität aufgrund der Abmilderung von Leistungseinbußen geben. Dies sollte sich jedoch auf die Fälle beschränken, in denen wir nach der Initialisierung alle Objekte unsterblich machen und sie später während der Laufzeitfinalisierung explizit freigeben. Der Code dafür sollte relativ konzentriert sein.
Spezifikation
Der Ansatz beinhaltet diese grundlegenden Änderungen
- füge _Py_IMMORTAL_REFCNT (den magischen Wert) zur internen C-API hinzu
- aktualisiere
Py_INCREF()undPy_DECREF()zu No-Ops für Objekte, die dem magischen Referenzzähler entsprechen - tu dasselbe für jede andere API, die den Referenzzähler ändert
- höre auf,
PyGC_Headfür unsterbliche GC-Objekte ("Container") zu modifizieren - stelle sicher, dass alle unsterblichen Objekte während der Laufzeitfinalisierung bereinigt werden
Dann macht das Setzen des Referenzzählers eines Objekts auf _Py_IMMORTAL_REFCNT es unsterblich.
(Es gibt andere kleinere, interne Änderungen, die hier nicht beschrieben werden.)
In den folgenden Unterabschnitten gehen wir auf die wichtigsten Details ein. Zuerst behandeln wir einige konzeptionelle Themen, gefolgt von konkreteren Aspekten wie spezifischen betroffenen APIs.
Details des öffentlichen Referenzzählers
In Abwärtskompatibilität haben wir mögliche Wege vorgestellt, wie Benutzercode durch die Änderung dieses Vorschlags beeinträchtigt werden könnte. Jedes beitragende Missverständnis von Benutzern ist wahrscheinlich zum großen Teil auf die Namen der Referenzzähler-bezogenen APIs und auf die Art und Weise zurückzuführen, wie die Dokumentation diese APIs (und die Referenzzählung im Allgemeinen) erklärt.
Zwischen den Namen und den Dokumenten können wir Antworten auf folgende Fragen klar erkennen:
- welches Verhalten erwarten die Benutzer?
- welche Garantien geben wir?
- zeigen wir an, wie der erhaltene Referenzzählerwert zu interpretieren ist?
- unter welchen Anwendungsfällen setzt ein Benutzer den Referenzzähler von Objekten, die er nicht erstellt hat?
- setzen Benutzer den Referenzzähler von Objekten, die sie nicht erstellt haben?
Als Teil dieses Vorschlags müssen wir sicherstellen, dass Benutzer klar verstehen können, auf welche Teile des Referenzzählerverhaltens sie sich verlassen können und welche als Implementierungsdetails gelten. Insbesondere sollten sie die vorhandenen öffentlichen Referenzzähler-bezogenen APIs verwenden, und die einzigen Referenzzählerwerte, die eine Bedeutung haben, sind 0 und 1. (Einige Codes verlassen sich auf 1 als Indikator dafür, dass das Objekt sicher modifiziert werden kann.) Alle anderen Werte gelten als "nicht 0 oder 1".
Diese Informationen werden in der Dokumentation klargestellt.
Man könnte argumentieren, dass die bestehenden Referenzzähler-bezogenen APIs geändert werden sollten, um das widerzuspiegeln, was wir von Benutzern erwarten. Etwas wie das Folgende:
Py_INCREF()->Py_ACQUIRE_REF()(oder nurPy_NewRef()unterstützen)Py_DECREF()->Py_RELEASE_REF()Py_REFCNT()->Py_HAS_REFS()Py_SET_REFCNT()->Py_RESET_REFS()undPy_SET_NO_REFS()
Eine solche Änderung ist jedoch kein Teil dieses Vorschlags. Sie ist hier enthalten, um den stärkeren Fokus auf Benutzererwartungen zu demonstrieren, der diese Änderung begünstigen würde.
Beschränkungen
- sicherstellen, dass ansonsten unveränderliche Objekte wirklich unveränderlich sein können
- die Leistungseinbußen für normale Python-Anwendungsfälle minimieren
- vorsichtig sein, wenn Objekte unsterblich gemacht werden, von denen wir nicht wirklich erwarten, dass sie bis zur Laufzeitfinalisierung bestehen bleiben.
- vorsichtig sein, wenn Objekte unsterblich gemacht werden, die ansonsten nicht unveränderlich sind
__del__und Weakrefs müssen weiterhin ordnungsgemäß funktionieren
In Bezug auf "wirklich" unveränderliche Objekte hat diese PEP keine Auswirkungen auf die effektive Unveränderlichkeit von Objekten, abgesehen vom laufzeitzustandsbezogenen Zustand pro Objekt (z. B. Referenzzähler). Ob ein unsterbliches Objekt wirklich (oder überhaupt effektiv) unveränderlich ist, kann daher separat von diesem Vorschlag geklärt werden. Zum Beispiel werden String-Objekte im Allgemeinen als unveränderlich betrachtet, aber PyUnicodeObject speichert einige lazy gecachte Daten. Diese PEP hat keinen Einfluss darauf, wie sich dieser Zustand auf die String-Unveränderlichkeit auswirkt.
Unsterbliche veränderliche Objekte
Jedes Objekt kann als unsterblich markiert werden. Wir schlagen keine Einschränkungen oder Prüfungen vor. In der Praxis hängt der Wert der Unsterblichmachung eines Objekts jedoch von seiner Veränderlichkeit und der Wahrscheinlichkeit ab, dass es für einen ausreichenden Teil der Lebensdauer der Anwendung verwendet wird. Die Markierung eines veränderlichen Objekts als unsterblich kann in einigen Situationen sinnvoll sein.
Viele der Anwendungsfälle für unsterbliche Objekte konzentrieren sich auf Unveränderlichkeit, damit Threads solche Objekte sicher und effizient ohne Sperren teilen können. Aus diesem Grund würde ein veränderliches Objekt wie ein Wörterbuch oder eine Liste niemals gemeinsam genutzt (und somit keine Unsterblichkeit). Unsterblichkeit kann jedoch angebracht sein, wenn eine ausreichende Garantie besteht, dass das normalerweise veränderliche Objekt tatsächlich nicht modifiziert wird.
Andererseits werden einige veränderliche Objekte niemals zwischen Threads gemeinsam genutzt (zumindest nicht ohne eine Sperre wie das GIL). In einigen Fällen kann es praktisch sein, auch einige davon unsterblich zu machen. Zum Beispiel ist sys.modules ein Pro-Interpreter-Wörterbuch, von dem wir nicht erwarten, dass es jemals freigegeben wird, bis der entsprechende Interpreter finalisiert wird (vorausgesetzt, es wird nicht ersetzt). Indem wir es unsterblich machen, würden wir den zusätzlichen Overhead bei incref/decref nicht mehr verursachen.
Wir untersuchen diese Idee weiter im Abschnitt Abhilfemaßnahmen unten.
Implizit unsterbliche Objekte
Wenn ein unsterbliches Objekt eine Referenz auf ein normales (sterbliches) Objekt hält, dann ist dieses gehaltene Objekt effektiv unsterblich. Das liegt daran, dass der Referenzzähler dieses Objekts niemals 0 erreichen kann, bis das unsterbliche Objekt es freigibt.
Beispiele
- Container wie
dictundlist - Objekte, die intern Referenzen halten, wie
PyTypeObjectmit seinentp_subclassesundtp_weaklist - der Typ eines Objekts (gehalten in
ob_type)
Solche gehaltenen Objekte sind somit effektiv unsterblich, solange sie gehalten werden. In der Praxis sollte dies keine wirklichen Konsequenzen haben, da es keine Verhaltensänderung darstellt. Der einzige Unterschied ist, dass das unsterbliche Objekt (das die Referenz hält) nie bereinigt wird.
Wir schlagen nicht vor, dass solche implizit unsterblichen Objekte in irgendeiner Weise geändert werden. Sie sollten nicht explizit als unsterblich markiert werden, nur weil sie von einem unsterblichen Objekt gehalten werden. Das würde keinen Vorteil gegenüber nichts tun bringen.
Objekte ent-unsterblich machen
Dieser Vorschlag beinhaltet keinen Mechanismus, um ein unsterbliches Objekt zu nehmen und es in einen "normalen" Zustand zurückzuversetzen. Derzeit besteht kein Bedarf an einer solchen Fähigkeit.
Darüber hinaus ist der offensichtliche Ansatz, einfach den Referenzzähler auf einen kleinen Wert zu setzen. Zu diesem Zeitpunkt gibt es jedoch keine Möglichkeit zu wissen, welcher Wert sicher wäre. Idealerweise würden wir ihn auf den Wert setzen, den er gehabt hätte, wenn er nicht unsterblich gemacht worden wäre. Dieser Wert wäre jedoch längst verloren. Daher machen die damit verbundenen Komplexitäten es unwahrscheinlicher, dass ein Objekt sicher ent-unsterblich gemacht werden könnte, selbst wenn wir einen guten Grund dazu hätten.
_Py_IMMORTAL_REFCNT
Wir werden zwei interne Konstanten hinzufügen
_Py_IMMORTAL_BIT - has the top-most available bit set (e.g. 2^62)
_Py_IMMORTAL_REFCNT - has the two top-most available bits set
Das tatsächliche oberste Bit hängt von bestehenden Verwendungen für Referenzzähler-Bits ab, z. B. dem Vorzeichenbit oder einigen GC-Verwendungen. Wir werden das höchstmögliche Bit verwenden, nachdem wir bestehende Verwendungen berücksichtigt haben.
Der Referenzzähler für unsterbliche Objekte wird auf _Py_IMMORTAL_REFCNT gesetzt (was bedeutet, dass der Wert auf halbem Weg zwischen _Py_IMMORTAL_BIT und dem Wert am nächsthöheren Bit liegt). Um jedoch zu prüfen, ob ein Objekt unsterblich ist, vergleichen wir (bitwise-and) seinen Referenzzähler mit nur _Py_IMMORTAL_BIT.
Der Unterschied bedeutet, dass ein unsterbliches Objekt immer noch als unsterblich gilt, selbst wenn sein Referenzzähler irgendwie geändert wurde (z. B. durch eine ältere Stable-ABI-Erweiterung).
Beachten Sie, dass die beiden obersten Bits des Referenzzählers bereits für andere Zwecke reserviert sind. Deshalb verwenden wir das drittoberste Bit.
Die Implementierung ist auch offen für die Verwendung anderer Werte für das unsterbliche Bit, wie z. B. das Vorzeichenbit oder 2^31 (für gesättigte Referenzzähler auf 64-Bit).
Betroffene API
APIs, die nun unsterbliche Objekte ignorieren werden
- (public)
Py_INCREF() - (public)
Py_DECREF() - (public)
Py_SET_REFCNT() - (private)
_Py_NewReference()
APIs, die Referenzzähler offenlegen (unverändert, aber können jetzt große Werte zurückgeben)
- (public)
Py_REFCNT() - (public)
sys.getrefcount()
(Beachten Sie, dass _Py_RefTotal und folglich sys.gettotalrefcount() nicht betroffen sein werden.)
TODO: Status von _Py_RefTotal klären.
Auch werden unsterbliche Objekte nicht am GC teilnehmen.
Unsterbliche globale Objekte
Alle Laufzeit-globalen (eingebauten) Objekte werden unsterblich gemacht. Dazu gehören die folgenden:
- Singletons (
None,True,False,Ellipsis,NotImplemented) - alle statischen Typen (z. B.
PyLong_Type,PyExc_Exception) - alle statischen Objekte in
_PyRuntimeState.global_objects(z.B. Bezeichner, kleine ganze Zahlen)
Die Frage, wie die vollständigen Objekte tatsächlich unveränderlich gemacht werden (z.B. für Interpreter-spezifische GIL), liegt außerhalb des Geltungsbereichs dieses PEPs.
Objektreinigung
Um alle unsterblichen Objekte während der Laufzeit-Finalisierung aufzuräumen, müssen wir sie verfolgen.
Für GC-Objekte ("Container") werden wir die permanente Generation des GC nutzen, indem wir alle "immortalisierten" Container dorthin verschieben. Während der Laufzeitabschaltung wird die Strategie darin bestehen, der Laufzeit zuerst zu erlauben, ihre bestmögliche Anstrengung zur normalen Freigabe dieser Instanzen zu unternehmen. Die meisten Modul-Freigaben werden nun von pylifecycle.c:finalize_modules() übernommen, wo wir die verbleibenden Module so gut wie möglich aufräumen. Dies wird ändern, welche Module während __del__ verfügbar sind, aber das ist laut Dokumentation bereits explizit undefiniertes Verhalten. Optional könnten wir eine topologische Sortierung durchführen, um sicherzustellen, dass Benutzer-Module vor den Standardbibliotheksmodulen freigegeben werden. Schließlich können alle übrig gebliebenen (falls vorhanden) über die Liste des permanenten GC gefunden werden, die wir nach Abschluss von finalize_modules() leeren können.
Für Nicht-Container-Objekte variiert der Verfolgungsansatz von Fall zu Fall. In fast jedem Fall ist ein solches Objekt direkt vom Laufzeitstatus zugänglich, z.B. in einem Feld von _PyRuntimeState oder PyInterpreterState. Für eine kleine Anzahl von Objekten müssen wir möglicherweise einen Tracking-Mechanismus zum Laufzeitstatus hinzufügen.
Keine der Bereinigungen wird signifikante Auswirkungen auf die Leistung haben.
Abmilderung von Leistungsregressionen
Zur Klarheit sind hier einige der Wege, auf denen wir versuchen werden, einen Teil der 4% Leistung zurückzugewinnen, die wir mit der naiven Implementierung unsterblicher Objekte verlieren.
Beachten Sie, dass keiner dieser Abschnitte tatsächlich Teil des Vorschlags ist.
markiere am Ende der Laufzeitinitialisierung alle Objekte als unsterblich
Wir können das Konzept aus Immortal Mutable Objects anwenden, um einen Teil der 4% Leistung zurückzugewinnen, die wir mit der naiven Implementierung unsterblicher Objekte verlieren. Am Ende der Laufzeitinitialisierung können wir *alle* Objekte als unsterblich markieren und die zusätzlichen Kosten für incref/decref vermeiden. Wir müssen uns nur um die Unveränderlichkeit bei Objekten kümmern, die wir zwischen Threads ohne GIL teilen wollen.
entferne unnötige fest codierte Referenzzähleroperationen
Teile der C-API interagieren spezifisch mit Objekten, von denen wir wissen, dass sie unsterblich sind, wie z.B. Py_RETURN_NONE. Solche Funktionen und Makros können aktualisiert werden, um Referenzzählungsoperationen wegzulassen.
spezialisiere für unsterbliche Objekte in der Auswertungs-Schleife
Es gibt Möglichkeiten, Operationen in der Ausführungsschleife zu optimieren, die spezifische, bekannte unsterbliche Objekte (z.B. None) betreffen. Der allgemeine Mechanismus wird in PEP 659 beschrieben. Siehe auch Pyston.
andere Möglichkeiten
- jede internierte Zeichenkette als unsterblich markieren
- das "interned" Dictionary als unsterblich markieren, wenn es geteilt wird, andernfalls alle interned Strings teilen
- (Larry,MAL) alle für ein Modul ent-serialisierten Konstanten als unsterblich markieren
- (Larry,MAL) (unveränderliche) unsterbliche Objekte auf einer eigenen Speicherseite/mehreren Speicherseiten zuweisen
- gesättigte Referenzzähler unter Verwendung der 32 niedrigstwertigen Bits
Lösungen für versehentliche Ent-Unsterblichmachung
Im Abschnitt Accidental De-Immortalizing haben wir eine mögliche negative Konsequenz von unsterblichen Objekten skizziert. Hier betrachten wir einige Optionen, um damit umzugehen.
Beachten Sie, dass wir hier Lösungen aufzählen, um zu veranschaulichen, dass zufriedenstellende Optionen verfügbar sind, anstatt vorzuschreiben, wie das Problem gelöst wird.
Beachten Sie auch Folgendes
- dies ist nur im 32-Bit Stable-ABI-Fall relevant
- es betrifft nur unsterbliche Objekte
- es gibt keine vom Benutzer definierten unsterblichen Objekte, nur eingebaute Typen
- die meisten unsterblichen Objekte werden statisch zugewiesen (und würden daher fehlschlagen, wenn
tp_dealloc()aufgerufen wird) - nur eine Handvoll unsterblicher Objekte wird oft genug verwendet, um dieses Problem in der Praxis möglicherweise zu haben (z.B.
None) - das Hauptproblem, das gelöst werden muss, sind Abstürze, die von
tp_dealloc()ausgehen
Eine grundlegende Beobachtung für eine Lösung ist, dass wir den Referenzzähler eines unsterblichen Objekts auf _Py_IMMORTAL_REFCNT zurücksetzen können, wenn eine bestimmte Bedingung erfüllt ist.
Mit all dem im Hinterkopf wäre eine einfache, aber effektive Lösung, den Referenzzähler eines unsterblichen Objekts in tp_dealloc() zurückzusetzen. NoneType und bool haben bereits eine tp_dealloc(), die Py_FatalError() aufruft, wenn sie ausgelöst wird. Dasselbe gilt für andere Typen unter bestimmten Bedingungen, wie PyUnicodeObject (abhängig von unicode_is_singleton()), PyTupleObject und PyTypeObject. Tatsächlich ist dieselbe Prüfung für alle statisch deklarierten Objekte wichtig. Für diese Typen würden wir stattdessen den Referenzzähler zurücksetzen. Für die verbleibenden Fälle würden wir die Prüfung einführen. In allen Fällen sollte der Overhead der Prüfung in tp_dealloc() zu gering sein, um ins Gewicht zu fallen.
Andere (weniger praktische) Lösungen
- den Referenzzähler für unsterbliche Objekte periodisch zurücksetzen
- nur für häufig genutzte Objekte
- nur tun, wenn eine Stable-ABI-Erweiterung importiert wurde
- ein Laufzeitflag zum Deaktivieren der Unsterblichkeit bereitstellen
(Die Diskussionsrunde enthält weitere Details.)
Unabhängig von der letztendlich gewählten Lösung können wir später bei Bedarf noch etwas anderes tun.
TODO: Eine Anmerkung hinzufügen, die darauf hinweist, dass die implementierte Lösung das allgemeine ~performance-neutrale~ Ergebnis nicht beeinträchtigt.
Dokumentation
Das Verhalten und die API für unsterbliche Objekte sind interne Implementierungsdetails und werden nicht in die Dokumentation aufgenommen.
Wir werden jedoch die Dokumentation aktualisieren, um öffentliche Garantien zum Verhalten der Referenzzählung klarer zu gestalten. Dies beinhaltet insbesondere
Py_INCREF()- ändern Sie "Inkrementieren Sie die Referenzanzahl für das Objekt o." zu "Zeigen Sie an, dass eine neue Referenz auf das Objekt o übernommen wird."Py_DECREF()- ändern Sie "Dekrementieren Sie die Referenzanzahl für das Objekt o." zu "Zeigen Sie an, dass eine zuvor übernommene Referenz auf das Objekt o nicht mehr verwendet wird."- ähnlich für
Py_XINCREF(),Py_XDECREF(),Py_NewRef(),Py_XNewRef(),Py_Clear() Py_REFCNT()- fügen Sie hinzu: "Die Referenzzähler 0 und 1 haben spezifische Bedeutungen, und alle anderen bedeuten nur, dass Code irgendwo das Objekt verwendet, unabhängig vom Wert. 0 bedeutet, dass das Objekt nicht verwendet wird und bereinigt wird. 1 bedeutet, dass der Code genau eine Referenz hält."Py_SET_REFCNT()- verweisen Sie aufPy_REFCNT(), wie Werte über 1 durch einen übergeordneten Wert ersetzt werden können
Wir *könnten* auch eine Anmerkung zu unsterblichen Objekten zu den folgenden hinzufügen, um Überraschungen für Benutzer zu vermeiden
Py_SET_REFCNT()(ein No-Op für unsterbliche Objekte)Py_REFCNT()(Wert kann überraschend hoch sein)sys.getrefcount()(Wert kann überraschend hoch sein)
Andere APIs, die von solchen Anmerkungen profitieren könnten, sind derzeit undokumentiert. Wir würden keine solche Anmerkung anderswo hinzufügen (auch nicht für Py_INCREF() und Py_DECREF()), da das Feature ansonsten für Benutzer transparent ist.
Referenzimplementierung
Die Implementierung wird auf GitHub vorgeschlagen
Offene Fragen
- wie realistisch ist die Sorge bezüglich Accidental De-Immortalizing?
Referenzen
Vorhandene Lösungen
Diskussionen
Dies wurde im Dezember 2021 auf python-dev diskutiert
Laufzeitobjektzustand
Hier ist der interne Zustand, den die CPython-Laufzeit für jedes Python-Objekt speichert
- PyObject.ob_refcnt: der Referenzzähler des Objekts
- _PyGC_Head: (optional) der Knoten des Objekts in einer Liste von „GC“-Objekten
- _PyObject_HEAD_EXTRA: (optional) der Knoten des Objekts in der Liste der Heap-Objekte
ob_refcnt ist Teil des Speichers, der für jedes Objekt zugewiesen wird. _PyObject_HEAD_EXTRA wird jedoch nur zugewiesen, wenn CPython mit Py_TRACE_REFS definiert wurde. PyGC_Head wird nur zugewiesen, wenn der Typ des Objekts Py_TPFLAGS_HAVE_GC gesetzt hat. Dies sind typischerweise nur Containertypen (z.B. list). Beachten Sie auch, dass PyObject.ob_refcnt und _PyObject_HEAD_EXTRA Teil von PyObject_HEAD sind.
Referenzzählung mit zyklischer Speicherbereinigung
Garbage Collection ist eine Speicherverwaltungsfunktion einiger Programmiersprachen. Das bedeutet, dass Objekte bereinigt werden (z.B. Speicher freigegeben), sobald sie nicht mehr benötigt werden.
Referenzzählung ist ein Ansatz zur Garbage Collection. Die Laufzeitumgebung verfolgt, wie viele Referenzen auf ein Objekt gehalten werden. Wenn Code die Eigentümerschaft einer Referenz auf ein Objekt übernimmt oder diese freigibt, wird die Laufzeit benachrichtigt, und sie inkrementiert oder dekrementiert den Referenzzähler entsprechend. Wenn der Referenzzähler 0 erreicht, bereinigt die Laufzeit das Objekt.
Bei CPython muss der Code explizit Referenzen über die C-API-Makros Py_INCREF() und Py_DECREF() übernehmen oder freigeben. Diese Makros modifizieren direkt den Referenzzähler des Objekts (unglücklicherweise, da dies ABI-Kompatibilitätsprobleme verursacht, wenn wir unser Garbage-Collection-Schema ändern wollen). Wenn ein Objekt in CPython bereinigt wird, gibt es auch alle von ihm gehaltenen Referenzen (und Ressourcen) frei (bevor sein Speicher freigegeben wird).
Manchmal können Objekte an Referenzzyklen beteiligt sein, z.B. wenn Objekt A eine Referenz auf Objekt B hält und Objekt B eine Referenz auf Objekt A hält. Folglich würde keines der Objekte jemals bereinigt, selbst wenn keine anderen Referenzen gehalten würden (d.h. ein Speicherleck). Die gängigsten Objekte, die an Zyklen beteiligt sind, sind Container.
CPython verfügt über spezielle Mechanismen, um mit Referenzzyklen umzugehen, die wir als "zyklischen Garbage Collector" oder oft einfach als "Garbage Collector" oder "GC" bezeichnen. Lassen Sie sich vom Namen nicht täuschen. Er befasst sich nur mit dem Brechen von Referenzzyklen.
Siehe die Dokumentation für eine detailliertere Erklärung von Referenzzählung und zyklischer Garbage Collection
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-0683.rst
Zuletzt geändert: 2024-06-12 18:00:45 GMT