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

Python Enhancement Proposals

PEP 523 – Hinzufügen einer Frame-Evaluierungs-API zu CPython

Autor:
Brett Cannon <brett at python.org>, Dino Viehland <dinoviehland at gmail.com>
Status:
Final
Typ:
Standards Track
Erstellt:
16. Mai 2016
Python-Version:
3.6
Post-History:
16. Mai 2016
Resolution:
Python-Dev Nachricht

Inhaltsverzeichnis

Zusammenfassung

Diese PEP schlägt vor, die C-API von CPython [2] zu erweitern, um die Angabe eines funktionszeigers pro Interpreter für die Auswertung von Frames [5] zu ermöglichen. Dieser Vorschlag schlägt auch vor, der Code-Objekte-Struktur [3] ein neues Feld hinzuzufügen, um beliebige Daten für die Verwendung durch die Frame-Auswertungsfunktion zu speichern.

Begründung

Ein Bereich, in dem es Python an Flexibilität mangelt, ist die direkte Ausführung von Python-Code. Während die C-API von CPython [2] es ermöglicht, die Daten, die in ein Frame-Objekt eingehen, zu konstruieren und diese dann über PyEval_EvalFrameEx() [5] auszuwerten, liegt die Kontrolle über die Ausführung von Python-Code bei einzelnen Objekten und nicht bei einer ganzheitlichen Kontrolle der Ausführung auf Frame-Ebene.

Obwohl der Wunsch, Einfluss auf die Frame-Auswertung zu nehmen, etwas zu niedrigschwellig erscheinen mag, eröffnet er die Möglichkeit, Dinge wie einen methodenbezogenen JIT in CPython einzuführen, ohne dass CPython selbst einen bereitstellen muss. Indem externem C-Code die Kontrolle über die Frame-Auswertung ermöglicht wird, kann ein JIT an der Ausführung von Python-Code an dem entscheidenden Punkt teilnehmen, an dem die Auswertung stattfindet. Dies ermöglicht einem JIT, Python-Bytecode nach Belieben bedingt in Maschinencode neu zu kompilieren, während weiterhin regulärer CPython-Bytecode ausgeführt wird, wenn die Ausführung des JIT nicht gewünscht ist. Dies kann erreicht werden, indem Interpretern erlaubt wird, anzugeben, welche Funktion zum Auswerten eines Frames aufgerufen werden soll. Und indem die API auf Frame-Auswertungsebene angesiedelt wird, erhält der JIT einen vollständigen Überblick über die Ausführungsumgebung des Codes.

Diese Fähigkeit, eine Frame-Auswertungsfunktion anzugeben, ermöglicht auch andere Anwendungsfälle, die über die reine Öffnung von CPython für JITs hinausgehen. Es wäre beispielsweise nicht schwierig, eine Trace- oder Profilierungsfunktion auf Aufrufebene mit dieser API zu implementieren. Während CPython die Möglichkeit bietet, eine Trace- oder Profilierungsfunktion auf Python-Ebene festzulegen, könnte dies die Datenerfassung des Profilers nachahmen und durch einfaches Überspringen der zeilenweisen Trace-Unterstützung wahrscheinlich schneller sein.

Es eröffnet auch die Möglichkeit des Debuggings, bei dem die Frame-Auswertungsfunktion nur dann spezielle Debugging-Arbeiten durchführt, wenn sie feststellt, dass sie einen bestimmten Code-Objekt ausführen wird. In diesem Fall könnte der Bytecode theoretisch vor Ort umgeschrieben werden, um einen Breakpoint-Funktionsaufruf an der richtigen Stelle einzufügen, um beim Debugging zu helfen, ohne einen schwerfälligen Ansatz verfolgen zu müssen, wie er von sys.settrace() gefordert wird.

Um diese Anwendungsfälle zu erleichtern, schlagen wir auch die Hinzufügung eines „Scratch-Bereichs“ zu Code-Objekten über ein neues Feld vor. Dies ermöglicht die Speicherung von Daten pro Code-Objekt direkt beim Code-Objekt für den einfachen Abruf durch die Frame-Auswertungsfunktion nach Bedarf. Das Feld selbst ist lediglich vom Typ PyObject *, sodass alle im Feld gespeicherten Daten am normalen Objekt-Speicherverwaltung teilnehmen.

Vorschlag

Alle vorgeschlagenen Änderungen an der C-API unten sind nicht Teil der stabilen ABI.

Erweiterung von PyCodeObject

Ein Feld wird der PyCodeObject-Struktur hinzugefügt [3]

typedef struct {
   ...
   void *co_extra;  /* "Scratch space" for the code object. */
} PyCodeObject;

Das Feld co_extra ist standardmäßig NULL und wird nur bei Bedarf gefüllt. Es wird erwartet, dass die im Feld gespeicherten Werte nicht erforderlich sind, damit das Code-Objekt funktioniert, was den Verlust der Daten des Feldes akzeptabel macht.

Eine private API wurde eingeführt, um mit dem Feld zu arbeiten

PyAPI_FUNC(Py_ssize_t) _PyEval_RequestCodeExtraIndex(freefunc);
PyAPI_FUNC(int) _PyCode_GetExtra(PyObject *code, Py_ssize_t index,
                                 void **extra);
PyAPI_FUNC(int) _PyCode_SetExtra(PyObject *code, Py_ssize_t index,
                                 void *extra);

Es wird erwartet, dass Benutzer des Feldes _PyEval_RequestCodeExtraIndex() aufrufen, um einen (als undurchsichtig zu betrachtenden) Indexwert für das Hinzufügen von Daten zu co-extra zu erhalten. Mit diesem Index können Benutzer Daten mit _PyCode_SetExtra() setzen und später mit _PyCode_GetExtra() abrufen. Die API ist bewusst als privat aufgeführt, um zu kommunizieren, dass es keine semantischen Garantien für die API zwischen Python-Versionen gibt.

Die Verwendung einer Liste und eines Tupels wurde in Betracht gezogen, fand aber weniger Leistung. Da ein Hauptanwendungsfall die JIT-Nutzung ist, hat die Leistungsbetrachtung bei der Verwendung einer benutzerdefinierten Struktur anstelle eines Python-Objekts Vorrang.

Ein Wörterbuch wurde ebenfalls in Betracht gezogen, aber wiederum war die Leistung wichtiger. Während ein Wörterbuch einen konstanten Overhead beim Nachschlagen von Daten hat, führt der Overhead für den häufigen Fall, dass ein einzelnes Objekt in der Datenstruktur gespeichert ist, dazu, dass ein Tupel bessere Leistungseigenschaften aufweist (d.h. die Iteration eines Tupels der Länge 1 ist schneller als der Overhead des Hashing und Nachschlagens eines Objekts in einem Wörterbuch).

Erweiterung von PyInterpreterState

Der Einstiegspunkt für die Frame-Auswertungsfunktion ist pro Interpreter

// Same type signature as PyEval_EvalFrameEx().
typedef PyObject* (*_PyFrameEvalFunction)(PyFrameObject*, int);

typedef struct {
    ...
    _PyFrameEvalFunction eval_frame;
} PyInterpreterState;

Standardmäßig wird das Feld eval_frame mit einem Funktionszeiger initialisiert, der dem entspricht, was PyEval_EvalFrameEx() derzeit ist (genannt _PyEval_EvalFrameDefault(), später in dieser PEP besprochen). Drittanbietercode kann dann seine eigene Frame-Auswertungsfunktion anstelle dessen festlegen, um die Ausführung von Python-Code zu steuern. Ein Zeigervergleich kann verwendet werden, um zu erkennen, ob das Feld auf _PyEval_EvalFrameDefault() gesetzt ist und somit noch nicht verändert wurde.

Änderungen an Python/ceval.c

PyEval_EvalFrameEx() [5] wird, wie sie derzeit besteht, in _PyEval_EvalFrameDefault() umbenannt. Das neue PyEval_EvalFrameEx() wird dann zu

PyObject *
PyEval_EvalFrameEx(PyFrameObject *frame, int throwflag)
{
    PyThreadState *tstate = PyThreadState_GET();
    return tstate->interp->eval_frame(frame, throwflag);
}

Dies ermöglicht Drittanbietercode, sich direkt in den Pfad der Python-Code-Ausführung zu begeben, während er abwärtskompatibel mit Code ist, der bereits die bestehende C-API verwendet.

Aktualisierung von python-gdb.py

Die generierte Datei python-gdb.py, die für die Python-Unterstützung in GDB verwendet wird, macht einige hartkodierte Annahmen über PyEval_EvalFrameEx(), z.B. die Namen lokaler Variablen. Sie muss aktualisiert werden, um mit den vorgeschlagenen Änderungen zu funktionieren.

Performance-Auswirkungen

Da diese PEP eine API zur Erweiterung der Plug-in-Fähigkeit vorschlägt, werden die Performance-Auswirkungen nur für den Fall berücksichtigt, dass kein Drittanbietercode Änderungen vorgenommen hat.

Mehrere Durchläufe von pybench [14] zeigten durchweg keine Leistungskosten allein durch die API-Änderung.

Ein Durchlauf der Python-Benchmark-Suite [9] zeigte keine messbaren Leistungskosten.

In Bezug auf den Speicherbedarf, da in einem Prozess typischerweise nicht viele CPython-Interpreter laufen, ist nur die Auswirkung von co_extra, das zu PyCodeObject hinzugefügt wird, bedenklich. Laut [8] führt ein Durchlauf der Python-Testsuite zu etwa 72.395 erstellten Code-Objekten. Auf einer 64-Bit-CPU würden dies 579.160 Bytes zusätzlichen Speicher bedeuten, wenn alle Code-Objekte gleichzeitig aktiv wären und nichts in ihren co_extra-Feldern gesetzt wäre.

Beispielverwendung

Ein JIT für CPython

Pyjion

Das Pyjion-Projekt [1] hat diese vorgeschlagene API verwendet, um einen JIT für CPython unter Verwendung des JIT von CoreCLR [4] zu implementieren. Jedes Code-Objekt hat sein Feld co_extra auf ein PyjionJittedCode-Objekt gesetzt, das vier Informationen speichert:

  1. Ausführungsanzahl
  2. Ein boolescher Wert, der angibt, ob ein früherer JIT-Versuch fehlgeschlagen ist
  3. Ein Funktionszeiger auf einen Trampolin (der typenverfolgend sein kann oder nicht)
  4. Ein void-Zeiger auf jeden JIT-kompilierten Maschinencode

Die Frame-Auswertungsfunktion hat (ungefähr) den folgenden Algorithmus

def eval_frame(frame, throw_flag):
    pyjion_code = frame.code.co_extra
    if not pyjion_code:
        frame.code.co_extra = PyjionJittedCode()
    elif not pyjion_code.jit_failed:
        if not pyjion_code.jit_code:
            return pyjion_code.eval(pyjion_code.jit_code, frame)
        elif pyjion_code.exec_count > 20_000:
            if jit_compile(frame):
                return pyjion_code.eval(pyjion_code.jit_code, frame)
            else:
                pyjion_code.jit_failed = True
    pyjion_code.exec_count += 1
    return _PyEval_EvalFrameDefault(frame, throw_flag)

Der entscheidende Punkt ist jedoch, dass all diese Arbeit und Logik von CPython getrennt ist, und doch ist es mit den vorgeschlagenen API-Änderungen möglich, einen JIT bereitzustellen, der mit Python-Semantik konform ist (zum Zeitpunkt der Erstellung ist die Leistung fast gleichwertig mit CPython ohne die neue API). Das bedeutet, dass technisch nichts andere davon abhält, eigene JITs für CPython zu implementieren, indem die vorgeschlagene API genutzt wird.

Andere JITs

Es sei erwähnt, dass das Pyston-Team zu einer früheren Version dieser PEP konsultiert wurde, die stärker JIT-spezifisch war, und kein Interesse an der Nutzung der Änderungen hatte, da sie die Kontrolle über das Speicherlayout wollten und kein direktes Interesse an der Unterstützung von CPython selbst hatten. Eine informelle Diskussion mit einem Entwickler des PyPy-Teams führte zu einer ähnlichen Bemerkung.

Numba [6] hingegen schlug vor, dass sie an der vorgeschlagenen Änderung in einer Post-1.0-Zukunft für sich selbst interessiert wären [7].

Der experimentelle Coconut JIT [13] hätte von dieser PEP profitiert. In privaten Gesprächen mit dem Schöpfer von Coconut wurde uns mitgeteilt, dass unsere API wahrscheinlich überlegen war gegenüber der, die sie für Coconut entwickelt hatten, um JIT-Unterstützung zu CPython hinzuzufügen.

Debugging

In Gesprächen mit dem Python Tools for Visual Studio-Team (PTVS) [12] dachten sie, dass sie diese API-Änderungen nützlich für die Implementierung performanterer Debugging-Tools finden würden. Wie im Abschnitt Begründung erwähnt, würde diese API ermöglichen, Debugging-Funktionalität nur in Frames zu aktivieren, wo sie benötigt wird. Dies könnte entweder das Überspringen von Informationen ermöglichen, die sys.settrace() normalerweise liefert, und sogar so weit gehen, Bytecode vor der Ausführung dynamisch umzuschreiben, um z.B. Breakpoints in den Bytecode einzufügen.

Es stellt sich auch heraus, dass Google intern eine sehr ähnliche API bereitstellt. Sie wurde für performante Debugging-Zwecke verwendet.

Implementierung

Eine Reihe von Patches, die die vorgeschlagene API implementieren, ist über das Pyjion-Projekt verfügbar [1]. In seiner aktuellen Form enthält es mehr Änderungen an CPython als nur diese vorgeschlagene API, aber das dient der einfacheren Entwicklung und nicht den strengen Anforderungen zur Erreichung seiner Ziele.

Offene Fragen

eval_frame auf NULL setzen

Derzeit wird erwartet, dass die Frame-Auswertungsfunktion immer gesetzt ist. Sie könnte sehr einfach standardmäßig auf NULL gesetzt werden, was die Verwendung von _PyEval_EvalFrameDefault() signalisieren würde. Der aktuelle Vorschlag, das Feld nicht speziell zu behandeln, schien am einfachsten, erfordert jedoch, dass das Feld nicht versehentlich gelöscht wird, da sonst ein Absturz auftreten kann.

Abgelehnte Ideen

Eine JIT-spezifische C-API

Ursprünglich sollte diese PEP eine wesentlich größere API-Änderung vorschlagen, die stärker JIT-spezifisch war. Nach Rückmeldung vom Numba-Team [6] wurde jedoch klar, dass die API unnötigerweise groß war. Es wurde erkannt, dass alles, was wirklich benötigt wurde, die Möglichkeit war, eine Trampolin-Funktion zur Ausführung von JIT-kompiliertem Python-Code bereitzustellen und eine Möglichkeit, diesen kompilierten Maschinencode zusammen mit anderen kritischen Daten an das entsprechende Python-Code-Objekt anzuhängen. Sobald gezeigt wurde, dass kein Verlust an Funktionalität oder Leistung auftrat, während die erforderlichen API-Änderungen minimiert wurden, wurde der Vorschlag in seine jetzige Form geändert.

Wird co_extra benötigt?

Bei der Diskussion dieser PEP auf der PyCon US 2016 äußerten einige Kernentwickler ihre Sorge, dass das Feld co_extra Code-Objekte veränderlich machen würde. Die Denkart schien zu sein, dass ein Feld, das nach der Erstellung des Code-Objekts verändert wurde, das Objekt als veränderlich erscheinen ließ, obwohl kein anderer Aspekt von Code-Objekten geändert wurde.

Die Ansicht dieser PEP ist, dass das Feld co_extra die Tatsache nicht ändert, dass Code-Objekte unveränderlich sind. Das Feld ist in dieser PEP so spezifiziert, dass es keine Informationen enthält, die für die Nutzbarkeit des Code-Objekts erforderlich sind, was es eher zu einem Cache-Feld macht. Es könnte als ähnlich zum UTF-8-Cache betrachtet werden, den String-Objekte intern haben; Strings gelten immer noch als unveränderlich, obwohl sie ein Feld haben, das bedingt gesetzt wird.

Es wurden auch Leistungsmessungen durchgeführt, bei denen das Feld für JIT-Workloads nicht verfügbar war. Der Verlust des Feldes erwies sich als zu kostspielig für die Leistung bei der Verwendung einer ungeordneten Map aus C++ oder Pythons Dict zur Zuordnung eines Code-Objekts zu JIT-spezifischen Datenobjekten.

Referenzen


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

Zuletzt geändert: 2025-10-03 20:38:03 GMT