PEP 590 – Vectorcall: ein schnelles Aufrufprotokoll für CPython
- Autor:
- Mark Shannon <mark at hotpy.org>, Jeroen Demeyer <J.Demeyer at UGent.be>
- BDFL-Delegate:
- Petr Viktorin <encukou at gmail.com>
- Status:
- Final
- Typ:
- Standards Track
- Erstellt:
- 29. März 2019
- Python-Version:
- 3.8
- Post-History:
Inhaltsverzeichnis
- Zusammenfassung
- Motivation
- Spezifikation
- Neue C-API und Änderungen an CPython
- Finalisierung der API
- Interne CPython-Änderungen
- Drittanbieter-Erweiterungsklassen mit Vectorcall
- Performance-Implikationen dieser Änderungen
- Stabile ABI
- Alternative Vorschläge
- Danksagungen
- Referenzen
- Referenzimplementierung
- Urheberrecht
Zusammenfassung
Diese PEP führt eine neue C-API ein, um Objektaufrufe zu optimieren. Sie führt ein neues „Vectorcall“-Protokoll und eine neue Aufrufkonvention ein. Dies basiert auf der „Fastcall“-Konvention, die bereits intern von CPython verwendet wird. Die neuen Funktionen können von jeder benutzerdefinierten Erweiterungsklasse verwendet werden.
Die meisten neuen API-Elemente sind in CPython 3.8 privat. Der Plan ist, die Semantik zu finalisieren und sie in Python 3.9 öffentlich zu machen.
HINWEIS: Diese PEP befasst sich ausschließlich mit der Python/C-API und hat keine Auswirkungen auf die Python-Sprache oder die Standardbibliothek.
Motivation
Die Wahl einer Aufrufkonvention beeinflusst die Leistung und Flexibilität von Code auf beiden Seiten des Aufrufs. Oft gibt es einen Konflikt zwischen Leistung und Flexibilität.
Die aktuelle tp_call [2] Aufrufkonvention ist flexibel genug, um alle Fälle abzudecken, aber ihre Leistung ist schlecht. Die schlechte Leistung ist größtenteils auf die Notwendigkeit zurückzuführen, während des Aufrufs Zwischen-Tupel und möglicherweise Zwischen-Dicts zu erstellen. Dies wird in CPython durch spezielle Code-Schnipsel zur Beschleunigung von Aufrufen an Python- und Builtin-Funktionen gemildert. Leider bedeutet dies, dass andere aufrufbare Objekte wie Klassen und Drittanbieter-Erweiterungsobjekte über die langsamere, allgemeinere tp_call Aufrufkonvention aufgerufen werden.
Diese PEP schlägt vor, die intern für Python- und Builtin-Funktionen verwendete Aufrufkonvention zu verallgemeinern und zu veröffentlichen, damit alle Aufrufe von besserer Leistung profitieren können. Die neue vorgeschlagene Aufrufkonvention ist nicht vollständig allgemein, deckt aber die große Mehrheit der Aufrufe ab. Sie ist darauf ausgelegt, den Overhead von temporären Objekten und mehrfachen Indirektionen zu entfernen.
Eine weitere Ineffizienz in der tp_call Konvention besteht darin, dass sie einen Funktionszeiger pro Klasse und nicht pro Objekt hat. Dies ist für Aufrufe von Klassen ineffizient, da mehrere Zwischenobjekte erstellt werden müssen. Für eine Klasse cls wird für jeden Aufruf in der Sequenz type.__call__, cls.__new__, cls.__init__ mindestens ein Zwischenobjekt erstellt.
Diese PEP schlägt eine Schnittstelle für die Verwendung durch Erweiterungsmodule vor. Solche Schnittstellen können nicht effektiv getestet oder entworfen werden, ohne die Verbraucher einzubeziehen. Aus diesem Grund stellen wir private (mit Unterstrich beginnende) Namen zur Verfügung. Die API kann sich (basierend auf dem Feedback der Verbraucher) in Python 3.9 ändern, wo wir erwarten, dass sie finalisiert wird und die Unterstriche entfernt werden.
Spezifikation
Der Funktionzeiger-Typ
Aufrufe erfolgen über einen Funktionzeiger, der die folgenden Parameter entgegennimmt
PyObject *callable: Das aufgerufene ObjektPyObject *const *args: Ein Vektor von Argumentensize_t nargs: Die Anzahl der Argumente plus das optionale FlagPY_VECTORCALL_ARGUMENTS_OFFSET(siehe unten)PyObject *kwnames: EntwederNULLoder ein Tupel mit den Namen der Schlüsselwortargumente
Dies wird durch den Funktionzeiger-Typ implementiert: typedef PyObject *(*vectorcallfunc)(PyObject *callable, PyObject *const *args, size_t nargs, PyObject *kwnames);
Änderungen an der PyTypeObject Struktur
Der ungenutzte Slot printfunc tp_print wird durch tp_vectorcall_offset ersetzt. Er hat den Typ Py_ssize_t. Ein neues tp_flags Flag wird hinzugefügt, _Py_TPFLAGS_HAVE_VECTORCALL, das für jede Klasse gesetzt sein muss, die das Vectorcall-Protokoll verwendet.
Wenn _Py_TPFLAGS_HAVE_VECTORCALL gesetzt ist, muss tp_vectorcall_offset eine positive ganze Zahl sein. Es ist der Offset in das Objekt des Vektorcall-Funktionzeigers vom Typ vectorcallfunc. Dieser Zeiger kann NULL sein, in diesem Fall ist das Verhalten dasselbe, als ob _Py_TPFLAGS_HAVE_VECTORCALL nicht gesetzt wäre.
Der tp_print Slot wird als tp_vectorcall_offset Slot wiederverwendet, um es externen Projekten zu erleichtern, das Vectorcall-Protokoll auf frühere Python-Versionen zurückzuporten. Insbesondere hat das Cython-Projekt Interesse daran gezeigt (siehe https://mail.python.org/pipermail/python-dev/2018-June/153927.html).
Deskriptor-Verhalten
Ein zusätzliches Typ-Flag wird spezifiziert: Py_TPFLAGS_METHOD_DESCRIPTOR.
Py_TPFLAGS_METHOD_DESCRIPTOR sollte gesetzt werden, wenn das aufrufbare Objekt das Deskriptor-Protokoll verwendet, um ein gebundenes Methoden-ähnliches Objekt zu erstellen. Dies wird vom Interpreter verwendet, um die Erstellung temporärer Objekte beim Aufruf von Methoden zu vermeiden (siehe _PyObject_GetMethod und die Opcodes LOAD_METHOD/CALL_METHOD).
Konkret, wenn Py_TPFLAGS_METHOD_DESCRIPTOR für type(func) gesetzt ist, dann
func.__get__(obj, cls)(*args, **kwds)(mitobjnicht None) muss äquivalent zufunc(obj, *args, **kwds)sein.func.__get__(None, cls)(*args, **kwds)muss äquivalent zufunc(*args, **kwds)sein.
Es gibt keine Einschränkungen für das Objekt func.__get__(obj, cls). Letzteres ist nicht verpflichtet, das Vectorcall-Protokoll zu implementieren.
Der Aufruf
Der Aufruf hat die Form ((vectorcallfunc)(((char *)o)+offset))(o, args, n, kwnames), wobei offset Py_TYPE(o)->tp_vectorcall_offset ist. Der Aufrufer ist dafür verantwortlich, das kwnames Tupel zu erstellen und sicherzustellen, dass keine Duplikate darin vorhanden sind.
n ist die Anzahl der positionsabhängigen Argumente plus möglicherweise das Flag PY_VECTORCALL_ARGUMENTS_OFFSET.
PY_VECTORCALL_ARGUMENTS_OFFSET
Das Flag PY_VECTORCALL_ARGUMENTS_OFFSET sollte zu n hinzugefügt werden, wenn der Aufgerufene erlaubt ist, args[-1] temporär zu ändern. Mit anderen Worten, dies kann verwendet werden, wenn args auf das Argument 1 im zugewiesenen Vektor zeigt. Der Aufgerufene muss den Wert von args[-1] vor der Rückgabe wiederherstellen.
Immer wenn sie es günstig tun können (ohne Allokation), werden Aufrufer ermutigt, PY_VECTORCALL_ARGUMENTS_OFFSET zu verwenden. Dies ermöglicht es aufrufbaren Objekten wie gebundenen Methoden, ihre nachfolgenden Aufrufe kostengünstig durchzuführen. Der Bytecode-Interpreter weist bereits Platz auf dem Stack für das aufrufbare Objekt zu, sodass er diesen Trick ohne zusätzliche Kosten nutzen kann.
Siehe [3] für ein Beispiel, wie PY_VECTORCALL_ARGUMENTS_OFFSET von einem Aufgerufenen verwendet wird, um Allokationen zu vermeiden.
Für die Ermittlung der tatsächlichen Anzahl von Argumenten aus dem Parameter n muss das Makro PyVectorcall_NARGS(n) verwendet werden. Dies ermöglicht zukünftige Änderungen oder Erweiterungen.
Neue C-API und Änderungen an CPython
Die folgenden Funktionen oder Makros werden zur C-API hinzugefügt
PyObject *_PyObject_Vectorcall(PyObject *obj, PyObject *const *args, size_t nargs, PyObject *keywords): Ruftobjmit den gegebenen Argumenten auf. Beachten Sie, dassnargsdas FlagPY_VECTORCALL_ARGUMENTS_OFFSETenthalten kann. Die tatsächliche Anzahl der positionsabhängigen Argumente wird durchPyVectorcall_NARGS(nargs)angegeben. Das Argumentkeywordsist ein Tupel von Schlüsselwortnamen oderNULL. Ein leeres Tupel hat die gleiche Wirkung wie die Übergabe vonNULL. Dies verwendet intern entweder das Vectorcall-Protokoll odertp_call. Wenn keines von beiden unterstützt wird, wird eine Ausnahme ausgelöst.PyObject *PyVectorcall_Call(PyObject *obj, PyObject *tuple, PyObject *dict): Ruft das Objekt (das Vectorcall unterstützen muss) mit der alten*argsund**kwargsAufrufkonvention auf. Dies ist hauptsächlich dazu gedacht, in dentp_callSlot gesetzt zu werden.Py_ssize_t PyVectorcall_NARGS(size_t nargs): Gibt die tatsächliche Anzahl der Argumente zurück, die aus einem VectorcallnargsArgument ermittelt wird. Derzeit äquivalent zunargs & ~PY_VECTORCALL_ARGUMENTS_OFFSET.
Unterklasse
Erweiterungstypen erben das Typ-Flag _Py_TPFLAGS_HAVE_VECTORCALL und den Wert tp_vectorcall_offset von der Basisklasse, vorausgesetzt, sie implementieren tp_call auf die gleiche Weise wie die Basisklasse. Zusätzlich wird das Flag Py_TPFLAGS_METHOD_DESCRIPTOR geerbt, wenn tp_descr_get auf die gleiche Weise wie die Basisklasse implementiert wird.
Heap-Typen erben nie das Vectorcall-Protokoll, da dies nicht sicher wäre (Heap-Typen können dynamisch geändert werden). Diese Einschränkung kann in Zukunft aufgehoben werden, erfordert aber eine Sonderbehandlung von __call__ in type.__setattribute__.
Finalisierung der API
Der Unterstrich in den Namen _PyObject_Vectorcall und _Py_TPFLAGS_HAVE_VECTORCALL weist darauf hin, dass sich diese API in kleineren Python-Versionen ändern kann. Nach der Finalisierung (geplant für Python 3.9) werden sie in PyObject_Vectorcall und Py_TPFLAGS_HAVE_VECTORCALL umbenannt. Die alten Namen mit Unterstrich bleiben als Aliase erhalten.
Die neue API wird wie gewohnt dokumentiert, wird aber auf das oben Genannte hinweisen.
Die Semantik für die anderen in dieser PEP eingeführten Namen (PyVectorcall_NARGS, PyVectorcall_Call, Py_TPFLAGS_METHOD_DESCRIPTOR, PY_VECTORCALL_ARGUMENTS_OFFSET) sind final.
Interne CPython-Änderungen
Änderungen an bestehenden Klassen
Die Klassen function, builtin_function_or_method, method_descriptor, method, wrapper_descriptor, method-wrapper werden das Vectorcall-Protokoll verwenden (nicht alle werden in der anfänglichen Implementierung geändert).
Für builtin_function_or_method und method_descriptor (die die PyMethodDef Datenstruktur verwenden), könnte man einen spezifischen Vectorcall-Wrapper für jede bestehende Aufrufkonvention implementieren. Ob sich das lohnt, bleibt abzuwarten.
Verwendung des Vectorcall-Protokolls für Klassen
Für eine Klasse cls erfordert die Erstellung einer neuen Instanz mit cls(xxx) mehrere Aufrufe. Mindestens ein Zwischenobjekt wird für jeden Aufruf in der Sequenz type.__call__, cls.__new__, cls.__init__ erstellt. Es ist daher sehr sinnvoll, Vectorcall für den Aufruf von Klassen zu verwenden. Dies bedeutet tatsächlich die Implementierung des Vectorcall-Protokolls für type. Einige der am häufigsten verwendeten Klassen werden dieses Protokoll verwenden, wahrscheinlich range, list, str und type.
Das PyMethodDef Protokoll und Argument Clinic
Argument Clinic [4] generiert automatisch Wrapper-Funktionen um Low-Level-Callables, die eine sichere Entpackung von primitiven Typen und andere Sicherheitsprüfungen ermöglichen. Argument Clinic könnte erweitert werden, um Wrapper-Objekte zu generieren, die dem neuen vectorcall Protokoll entsprechen. Dies ermöglicht es der Ausführung, vom Aufrufer zum von Argument Clinic generierten Wrapper und dann zum handgeschriebenen Code mit nur einer einzigen Indirektion zu fließen.
Drittanbieter-Erweiterungsklassen mit Vectorcall
Um eine Aufrufeffizienz zu erreichen, die mit Python- und Builtin-Funktionen vergleichbar ist, sollten Drittanbieter-Callables einen vectorcallfunc Funktionzeiger enthalten, tp_vectorcall_offset auf den korrekten Wert setzen und das Flag _Py_TPFLAGS_HAVE_VECTORCALL hinzufügen. Jede Klasse, die dies tut, muss die Funktion tp_call implementieren und sicherstellen, dass ihr Verhalten mit der Funktion vectorcallfunc konsistent ist. Das Setzen von tp_call auf PyVectorcall_Call ist ausreichend.
Performance-Implikationen dieser Änderungen
Diese PEP sollte keine großen Auswirkungen auf die Leistung bestehenden Codes haben (weder positiv noch negativ). Sie ist hauptsächlich dazu gedacht, die Erstellung von effizientem neuem Code zu ermöglichen und nicht, bestehenden Code schneller zu machen.
Nichtsdestotrotz optimiert diese PEP Funktionen vom Typ METH_FASTCALL. Die Leistung von Funktionen, die METH_VARARGS verwenden, wird geringfügig schlechter.
Stabile ABI
Nichts aus dieser PEP wird der stabilen ABI (PEP 384) hinzugefügt.
Alternative Vorschläge
bpo-29259
PEP 590 ähnelt dem in bpo-29259 [1] vorgeschlagenen. Der Hauptunterschied besteht darin, dass diese PEP den Funktionzeiger in der Instanz statt in der Klasse speichert. Dies ist sinnvoller für die Implementierung von Funktionen in C, wo jede Instanz einer anderen C-Funktion entspricht. Es ermöglicht auch die Optimierung von type.__call__, was mit bpo-29259 nicht möglich ist.
PEP 576 und PEP 580
Sowohl PEP 576 als auch PEP 580 zielen darauf ab, 3rd-Party-Objekte sowohl ausdrucksstark als auch performant (auf Augenhöhe mit CPython-Objekten) zu machen. Der Zweck dieser PEP ist es, eine einheitliche Methode für den Aufruf von Objekten im CPython-Ökosystem bereitzustellen, die sowohl ausdrucksstark als auch so performant wie möglich ist.
Diese PEP hat einen breiteren Anwendungsbereich als PEP 576 und verwendet variable statt feste Offset-Funktionzeiger. Die zugrunde liegende Aufrufkonvention ist ähnlich. Da PEP 576 nur einen festen Offset für den Funktionzeiger zulässt, wären die Verbesserungen für Objekte mit Einschränkungen in ihrem Layout nicht möglich.
PEP 580 schlägt eine größere Änderung des PyMethodDef Protokolls vor, das zur Definition von Builtin-Funktionen verwendet wird. Diese PEP bietet einen allgemeineren und einfacheren Mechanismus in Form einer neuen Aufrufkonvention. Diese PEP erweitert auch das PyMethodDef Protokoll, aber nur, um bestehende Konventionen zu formalisieren.
Andere abgelehnte Ansätze
Eine längere Form mit 6 Argumenten, die sowohl Vektor- als auch optionale Tupel- und Dictionary-Argumente kombiniert, wurde in Betracht gezogen. Es wurde jedoch festgestellt, dass der Code zur Konvertierung zwischen ihr und der alten tp_call Form übermäßig umständlich und ineffizient war. Da auf x64 Windows nur 4 Argumente in Registern übergeben werden, hätten die zwei zusätzlichen Argumente nicht unerhebliche Kosten verursacht.
Das Entfernen aller Sonderfälle und die Verwendung der tp_call Form für alle Aufrufe wurde ebenfalls in Betracht gezogen. Wenn jedoch keine wesentlich effizientere Methode zur Erstellung und Zerstörung von Tupeln und, in geringerem Maße, von Dictionaries gefunden worden wäre, wäre dies zu langsam gewesen.
Danksagungen
Victor Stinner für die Entwicklung der ursprünglichen "Fastcall"-Aufrufkonvention intern in CPython. Diese PEP kodifiziert und erweitert seine Arbeit.
Referenzen
Referenzimplementierung
Eine minimale Implementierung finden Sie unter https://github.com/markshannon/cpython/tree/vectorcall-minimal
Urheberrecht
Dieses Dokument wurde gemeinfrei erklärt.
Quelle: https://github.com/python/peps/blob/main/peps/pep-0590.rst
Zuletzt geändert: 2025-07-14 10:52:37 GMT