PEP 510 – Spezialisierte Funktionen mit Guards
- Autor:
- Victor Stinner <vstinner at python.org>
- Status:
- Abgelehnt
- Typ:
- Standards Track
- Erstellt:
- 04-Jan-2016
- Python-Version:
- 3.6
Ablehnungsbescheid
Diese PEP wurde von ihrem Autor abgelehnt, da das Design keine signifikanten Geschwindigkeitssteigerungen zeigte, aber auch aufgrund fehlender Zeit, die fortschrittlichsten und komplexesten Optimierungen zu implementieren.
Zusammenfassung
Fügen Sie Funktionen zur Python C API hinzu, um reine Python-Funktionen zu spezialisieren: Hinzufügen von spezialisierten Codes mit Guards. Dies ermöglicht die Implementierung statischer Optimizer, die die Python-Semantik respektieren.
Begründung
Python Semantik
Python ist schwer zu optimieren, da fast alles veränderlich ist: eingebaute Funktionen, Funktionscodes, globale Variablen, lokale Variablen, ... können zur Laufzeit geändert werden. Die Implementierung von Optimierungen, die die Python-Semantik respektieren, erfordert die Erkennung, wenn „etwas sich ändert“. Wir werden diese Prüfungen als „Guards“ bezeichnen.
Diese PEP schlägt vor, eine öffentliche API zur Python C API hinzuzufügen, um spezialisierte Codes mit Guards zu einer Funktion hinzuzufügen. Wenn die Funktion aufgerufen wird, wird ein spezialisierter Code verwendet, wenn nichts geändert wurde, andernfalls wird der ursprüngliche Bytecode verwendet.
Auch wenn Guards helfen, die meisten Teile der Python-Semantik zu respektieren, ist es schwer, Python zu optimieren, ohne subtile Änderungen am exakten Verhalten vorzunehmen. CPython hat eine lange Geschichte und viele Anwendungen verlassen sich auf Implementierungsdetails. Es muss ein Kompromiss zwischen „alles ist veränderlich“ und Leistung gefunden werden.
Das Schreiben eines Optimierers liegt außerhalb des Umfangs dieser PEP.
Warum kein JIT-Compiler?
Es gibt mehrere JIT-Compiler für Python, die aktiv entwickelt werden
Numba ist speziell für numerische Berechnungen. Pyston und Pyjion sind noch jung. PyPy ist der vollständigste Python-Interpreter, er ist im Allgemeinen schneller als CPython in Mikro- und vielen Makro-Benchmarks und hat eine sehr gute Kompatibilität mit CPython (er respektiert die Python-Semantik). Es gibt immer noch Probleme mit Python JIT-Compilern, die ihre breite Verwendung anstelle von CPython verhindern.
Viele beliebte Bibliotheken wie numpy, PyGTK, PyQt, PySide und wxPython sind in C oder C++ implementiert und verwenden die Python C API. Um einen geringen Speicherbedarf und bessere Leistung zu erzielen, verwenden Python JIT-Compiler keine Referenzzählung, um einen schnelleren Garbage Collector zu verwenden, sie verwenden keine C-Strukturen von CPython-Objekten und verwalten Speicherzuweisungen anders. PyPy verfügt über ein cpyext-Modul, das die Python C API emuliert, aber es hat schlechtere Leistungen als CPython und unterstützt nicht die vollständige Python C API.
Neue Funktionen werden zuerst in CPython entwickelt. Im Januar 2016 ist die neueste stabile CPython-Version 3.5, während PyPy nur Python 2.7 und 3.2 unterstützt und Pyston nur Python 2.7 unterstützt.
Auch wenn PyPy eine sehr gute Kompatibilität mit Python hat, sind einige Module immer noch nicht mit PyPy kompatibel: siehe PyPy Compatibility Wiki. Die unvollständige Unterstützung der Python C API ist Teil dieses Problems. Es gibt auch subtile Unterschiede zwischen PyPy und CPython wie die Referenzzählung: Objekt-Destruktoren werden in PyPy immer aufgerufen, können aber „später“ aufgerufen werden als in CPython. Die Verwendung von Kontextmanagern hilft, zu kontrollieren, wann Ressourcen freigegeben werden.
Auch wenn PyPy in einer Vielzahl von Benchmarks viel schneller als CPython ist, berichten einige Benutzer immer noch über schlechtere Leistungen als CPython in einigen spezifischen Anwendungsfällen oder instabile Leistungen.
Wenn Python als Skriptprogramm für Programme verwendet wird, die weniger als 1 Minute laufen, können JIT-Compiler langsamer sein, da ihre Startzeit höher ist und der JIT-Compiler Zeit für die Optimierung des Codes benötigt. Zum Beispiel dauern die meisten Mercurial-Befehle nur wenige Sekunden.
Numba unterstützt jetzt Ahead-of-Time-Kompilierung, erfordert aber Dekoratoren zur Angabe von Argumenttypen und unterstützt nur numerische Typen.
CPython 3.5 hat fast keine Optimierung: Der Peephole-Optimizer implementiert nur grundlegende Optimierungen. Ein statischer Compiler ist ein Kompromiss zwischen CPython 3.5 und PyPy.
Hinweis
Es gab auch das Projekt Unladen Swallow, aber es wurde 2011 aufgegeben.
Beispiele
Die folgenden Beispiele sind nicht dazu geschrieben, leistungsstarke Optimierungen mit wichtigen Geschwindigkeitssteigerungen zu zeigen, sondern kurz und leicht verständlich zu sein, nur um das Prinzip zu erklären.
Hypothetisches myoptimizer Modul
Beispiele in dieser PEP verwenden ein hypothetisches myoptimizer-Modul, das die folgenden Funktionen und Typen bereitstellt
specialize(func, code, guards): Fügt den spezialisierten Codecodemit Guardsguardszur Funktionfunchinzuget_specialized(func): Ruft die Liste der spezialisierten Codes als Liste von(code, guards)-Tupeln ab, wobeicodeein Callable oder ein Code-Objekt ist undguardseine Liste von Guards istGuardBuiltins(name): Guard, derbuiltins.__dict__[name]undglobals()[name]überwacht. Der Guard schlägt fehl, wennbuiltins.__dict__[name]ersetzt wird oder wennglobals()[name]zugewiesen wird.
Verwendung von Bytecode
Fügt spezialisierten Bytecode hinzu, bei dem der Aufruf der reinen eingebauten Funktion chr(65) durch ihr Ergebnis "A" ersetzt wird
import myoptimizer
def func():
return chr(65)
def fast_func():
return "A"
myoptimizer.specialize(func, fast_func.__code__,
[myoptimizer.GuardBuiltins("chr")])
del fast_func
Beispiel, das das Verhalten des Guards zeigt
print("func(): %s" % func())
print("#specialized: %s" % len(myoptimizer.get_specialized(func)))
print()
import builtins
builtins.chr = lambda obj: "mock"
print("func(): %s" % func())
print("#specialized: %s" % len(myoptimizer.get_specialized(func)))
Ausgabe
func(): A
#specialized: 1
func(): mock
#specialized: 0
Der erste Aufruf verwendet den spezialisierten Bytecode, der den String "A" zurückgibt. Der zweite Aufruf entfernt den spezialisierten Code, da die eingebaute Funktion chr() ersetzt wurde, und führt den ursprünglichen Bytecode aus, der chr(65) aufruft.
In einem Mikrobenchmark dauert der Aufruf des spezialisierten Bytecodes 88 ns, während die ursprüngliche Funktion 145 ns (+57 ns) benötigt: 1,6-mal schneller.
Verwendung einer eingebauten Funktion
Fügt die C-eingebaute Funktion chr() als spezialisierten Code hinzu, anstatt eines Bytecodes, der chr(obj) aufruft
import myoptimizer
def func(arg):
return chr(arg)
myoptimizer.specialize(func, chr,
[myoptimizer.GuardBuiltins("chr")])
Beispiel, das das Verhalten des Guards zeigt
print("func(65): %s" % func(65))
print("#specialized: %s" % len(myoptimizer.get_specialized(func)))
print()
import builtins
builtins.chr = lambda obj: "mock"
print("func(65): %s" % func(65))
print("#specialized: %s" % len(myoptimizer.get_specialized(func)))
Ausgabe
func(): A
#specialized: 1
func(): mock
#specialized: 0
Der erste Aufruf ruft die C-eingebaute Funktion chr() auf (ohne einen Python-Frame zu erstellen). Der zweite Aufruf entfernt den spezialisierten Code, da die eingebaute Funktion chr() ersetzt wurde, und führt den ursprünglichen Bytecode aus.
In einem Mikrobenchmark dauert der Aufruf des C-eingebauten Funktions 95 ns, während der ursprüngliche Bytecode 155 ns (+60 ns) benötigt: 1,6-mal schneller. Der direkte Aufruf von chr(65) dauert 76 ns.
Auswahl des spezialisierten Codes
Pseudocode zur Auswahl des spezialisierten Codes für den Aufruf einer reinen Python-Funktion
def call_func(func, args, kwargs):
specialized = myoptimizer.get_specialized(func)
nspecialized = len(specialized)
index = 0
while index < nspecialized:
specialized_code, guards = specialized[index]
for guard in guards:
check = guard(args, kwargs)
if check:
break
if not check:
# all guards succeeded:
# use the specialized code
return specialized_code
elif check == 1:
# a guard failed temporarily:
# try the next specialized code
index += 1
else:
assert check == 2
# a guard will always fail:
# remove the specialized code
del specialized[index]
# if a guard of each specialized code failed, or if the function
# has no specialized code, use original bytecode
code = func.__code__
Änderungen
Änderungen an der Python C API
- Fügt ein
PyFuncGuardObject-Objekt und einenPyFuncGuard_Type-Typ hinzu - Fügt eine
PySpecializedCode-Struktur hinzu - Fügt die folgenden Felder zur
PyFunctionObject-Struktur hinzuPy_ssize_t nb_specialized; PySpecializedCode *specialized;
- Fügt Funktionsmethoden hinzu
PyFunction_Specialize()PyFunction_GetSpecializedCodes()PyFunction_GetSpecializedCode()PyFunction_RemoveSpecialized()PyFunction_RemoveAllSpecialized()
Keine dieser Funktionen und Typen wird auf Python-Ebene verfügbar gemacht.
Alle diese Ergänzungen sind ausdrücklich von der stabilen ABI ausgeschlossen.
Wenn ein Funktionscode ersetzt wird (func.__code__ = new_code), werden alle spezialisierten Codes und Guards entfernt.
Funktions-Guard
Fügt ein Funktions-Guard-Objekt hinzu
typedef struct {
PyObject ob_base;
int (*init) (PyObject *guard, PyObject *func);
int (*check) (PyObject *guard, PyObject **stack, int na, int nk);
} PyFuncGuardObject;
Die Funktion init() initialisiert einen Guard
- Gibt
0bei Erfolg zurück - Gibt
1zurück, wenn der Guard immer fehlschlägt:PyFunction_Specialize()muss den spezialisierten Code ignorieren - Löst eine Ausnahme aus und gibt
-1bei einem Fehler zurück
Die Funktion check() prüft einen Guard
- Gibt
0bei Erfolg zurück - Gibt
1zurück, wenn der Guard vorübergehend fehlgeschlagen ist - Gibt
2zurück, wenn der Guard immer fehlschlägt: Der spezialisierte Code muss entfernt werden - Löst eine Ausnahme aus und gibt
-1bei einem Fehler zurück
stack ist ein Array von Argumenten: indizierte Argumente gefolgt von (key, value)-Paaren von Keyword-Argumenten. na ist die Anzahl der indizierten Argumente. nk ist die Anzahl der Keyword-Argumente: die Anzahl der (key, value)-Paare. stack enthält na + nk * 2 Objekte.
Spezialisierter Code
Fügt eine spezialisierte Code-Struktur hinzu
typedef struct {
PyObject *code; /* callable or code object */
Py_ssize_t nb_guard;
PyObject **guards; /* PyFuncGuardObject objects */
} PySpecializedCode;
Funktionsmethoden
PyFunction_Specialize
Fügt eine Funktionmethode hinzu, um die Funktion zu spezialisieren und einen spezialisierten Code mit Guards hinzuzufügen
int PyFunction_Specialize(PyObject *func,
PyObject *code, PyObject *guards)
Wenn code eine Python-Funktion ist, wird das Code-Objekt der code-Funktion als spezialisierter Code verwendet. Die spezialisierte Python-Funktion muss dieselben Standardwerte für Parameter, dieselben Standardwerte für Schlüsselwortparameter haben und darf keinen spezialisierten Code haben.
Wenn code eine Python-Funktion oder ein Code-Objekt ist, wird ein neues Code-Objekt erstellt und der Code-Name und die erste Zeilennummer des Code-Objekts von func kopiert. Der spezialisierte Code muss dieselben Zellvariablen und dieselben freien Variablen haben.
Ergebnis
- Gibt
0bei Erfolg zurück - Gibt
1zurück, wenn die Spezialisierung ignoriert wurde - Löst eine Ausnahme aus und gibt
-1bei einem Fehler zurück
PyFunction_GetSpecializedCodes
Fügt eine Funktionmethode hinzu, um die Liste der spezialisierten Codes abzurufen
PyObject* PyFunction_GetSpecializedCodes(PyObject *func)
Gibt eine Liste von (code, guards)-Tupeln zurück, wobei code ein Callable oder ein Code-Objekt und guards eine Liste von PyFuncGuard-Objekten ist. Löst eine Ausnahme aus und gibt NULL bei einem Fehler zurück.
PyFunction_GetSpecializedCode
Fügt eine Funktionmethode hinzu, die Guards prüft, um einen spezialisierten Code auszuwählen
PyObject* PyFunction_GetSpecializedCode(PyObject *func,
PyObject **stack,
int na, int nk)
Siehe die Funktion check() der Guards für die Argumente stack, na und nk. Gibt bei Erfolg ein Callable oder ein Code-Objekt zurück. Löst eine Ausnahme aus und gibt NULL bei einem Fehler zurück.
PyFunction_RemoveSpecialized
Fügt eine Funktionmethode hinzu, um einen spezialisierten Code mit seinen Guards anhand seines Index zu entfernen
int PyFunction_RemoveSpecialized(PyObject *func, Py_ssize_t index)
Gibt 0 bei Erfolg oder wenn der Index nicht existiert zurück. Löst eine Ausnahme aus und gibt -1 bei einem Fehler zurück.
PyFunction_RemoveAllSpecialized
Fügt eine Funktionmethode hinzu, um alle spezialisierten Codes und Guards einer Funktion zu entfernen
int PyFunction_RemoveAllSpecialized(PyObject *func)
Gibt 0 bei Erfolg zurück. Löst eine Ausnahme aus und gibt -1 zurück, wenn func keine Funktion ist.
Benchmark
Mikrobenchmark auf python3.6 -m timeit -s 'def f(): pass' 'f()' (Best of 3 Runs)
- Original Python: 79 ns
- Gepatchtes Python: 79 ns
Laut diesem Mikrobenchmark haben die Änderungen keinen Overhead beim Aufruf einer Python-Funktion ohne Spezialisierung.
Implementierung
Das Issue #26098: PEP 510: Specialize functions with guards enthält einen Patch, der diese PEP implementiert.
Andere Implementierungen von Python
Diese PEP enthält nur Änderungen an der Python C API, die Python API ist unverändert. Andere Implementierungen von Python können neue Ergänzungen nach Belieben nicht implementieren oder hinzugefügte Funktionen als No-Ops implementieren.
PyFunction_Specialize(): Gibt immer1zurück (die Spezialisierung wurde ignoriert)PyFunction_GetSpecializedCodes(): Gibt immer eine leere Liste zurückPyFunction_GetSpecializedCode(): Gibt das Funktions-Code-Objekt zurück, wie das vorhandene MakroPyFunction_GET_CODE()
Diskussion
Thread in der python-ideas Mailingliste: RFC: PEP: Specialized functions with guards.
Urheberrecht
Dieses Dokument wurde gemeinfrei erklärt.
Quelle: https://github.com/python/peps/blob/main/peps/pep-0510.rst
Zuletzt geändert: 2025-02-01 08:59:27 GMT