PEP 329 – Builtin-Referenzen als Konstanten in der Standardbibliothek behandeln
- Autor:
- Raymond Hettinger <python at rcn.com>
- Status:
- Abgelehnt
- Typ:
- Standards Track
- Erstellt:
- 18. Apr. 2004
- Python-Version:
- 2.4
- Post-History:
- 18. Apr. 2004
Zusammenfassung
Der Vorschlag besteht darin, eine Funktion zum Behandeln von Builtin-Referenzen als Konstanten hinzuzufügen und diese Funktion in der gesamten Standardbibliothek anzuwenden.
Status
Das PEP wurde vom Autor selbst zurückgewiesen. Obwohl das ASPN-Rezept gut aufgenommen wurde, gab es weniger Bereitschaft, dies für die Aufnahme in die Kernverteilung in Betracht zu ziehen.
Die Jython-Implementierung verwendet keine Bytecodes. Daher würde ihre Leistung leiden, wenn die aktuellen _len=len Optimierungen entfernt würden.
Außerdem ist die Änderung von Bytecodes eine der unsaubersten Methoden zur Leistungssteigerung und Ermöglichung saubererer Codierung. Eine robustere Lösung würde wahrscheinlich Compiler-Pragma-Direktiven oder Metavariablen umfassen, die angeben, was optimiert werden kann (ähnlich wie const/volatile-Deklarationen).
Motivation
Die Bibliothek enthält Code wie _len=len, der darauf abzielt, schnelle lokale Referenzen anstelle langsamer globaler Lookups zu erstellen. Obwohl für die Leistung notwendig, verunreinigen diese Konstrukte den Code und sind meist unvollständig (verpassen viele Gelegenheiten).
Wenn der Vorschlag angenommen wird, könnten diese Konstrukte aus der Codebasis eliminiert und gleichzeitig ihre Ergebnisse in Bezug auf die Leistung verbessert werden.
Derzeit gibt es über hundert Fälle von while 1 in der Bibliothek. Sie wurden aus Leistungsgründen nicht durch das lesbarere while True ersetzt (der Compiler kann den Test nicht eliminieren, da True nicht als immer konstant bekannt ist). Die Umwandlung von True in eine Konstante wird den Code klären und gleichzeitig die Leistung beibehalten.
Viele andere grundlegende Python-Operationen sind aufgrund globaler Lookups viel langsamer. In try/except-Anweisungen werden die abgefangenen Ausnahmen dynamisch nachgeschlagen, bevor geprüft wird, ob sie übereinstimmen. Ähnlich erfordern einfache Identitätstests wie while x is not None, dass die Variable None bei jedem Durchlauf neu nachgeschlagen wird. Builtin-Lookups sind besonders gravierend, da zuerst der umschließende globale Geltungsbereich geprüft werden muss. Diese Lookup-Ketten verbrauchen Cache-Speicher, der besser woanders verwendet werden könnte.
Kurz gesagt, wenn der Vorschlag angenommen wird, wird der Code sauberer und die Leistung verbessert sich insgesamt.
Vorschlag
Fügen Sie ein Modul namens codetweaks.py hinzu, das zwei Funktionen enthält: bind_constants() und bind_all(). Die erste Funktion führt die konstante Bindung durch und die zweite wendet sie rekursiv auf jede Funktion und Klasse in einem Zielmodul an.
Fügen Sie für die meisten Module in der Standardbibliothek am Ende des Skripts ein Paar Zeilen hinzu
import codetweaks, sys
codetweaks.bind_all(sys.modules[__name__])
Neben der Bindung von Builtins gibt es einige Module (wie sre_compile), in denen es auch sinnvoll ist, Modulvariablen sowie Builtins in Konstanten zu binden.
Fragen und Antworten
- Wird das alle dazu bringen, ihre Aufmerksamkeit auf Optimierungsprobleme zu richten?
Da dies automatisch geschieht, reduziert es die Notwendigkeit, über Optimierungen nachzudenken.
- Wie funktioniert das im Grunde?
Jede Funktion hat Attribute mit ihren Bytecodes (der Sprache der Python-Virtuellen Maschine) und eine Tabelle von Konstanten. Die Bindungsfunktion scannt die Bytecodes nach einer
LOAD_GLOBALAnweisung und prüft, ob der Wert bereits bekannt ist. Wenn ja, fügt sie diesen Wert der Konstanten-Tabelle hinzu und ersetzt den Opcode durchLOAD_CONSTANT. - Wann funktioniert das?
Wenn ein Modul zum ersten Mal importiert wird, kompiliert Python den Bytecode und führt die Bindungsoptimierung durch. Nachfolgende Importe verwenden einfach die vorherige Arbeit wieder. Jede Sitzung wiederholt diesen Prozess (die Ergebnisse werden nicht in
pycDateien gespeichert). - Woher weißt du, dass das funktioniert?
Ich habe es implementiert, auf jedes Modul in der Bibliothek angewendet, und die Testsuite lief ohne Ausnahme durch.
- Was ist, wenn das Modul eine Variable definiert, die eine Builtin überschattet?
Das passiert. Zum Beispiel kann True auf Modulebene als
True = (1==1)neu definiert werden. Die untenstehende Beispielimplementierung erkennt die Überschattung und lässt den globalen Lookup unverändert. - Sind Sie die erste Person, die erkennt, dass die meisten globalen Lookups für Werte sind, die sich nie ändern?
Nein, das ist seit langem bekannt. Skip Montanaro gibt eine eloquente Erklärung in PEP 266.
- Was ist, wenn ich das Builtins-Modul ersetzen und meine eigenen Implementierungen bereitstellen möchte?
Tun Sie dies entweder, bevor Sie ein Modul importieren, oder laden Sie das Modul einfach neu, oder deaktivieren Sie
codetweaks.py(es wird eine Deaktivierungsflagge haben). - Wie anfällig ist dieses Modul für Änderungen in Pythons Bytecodierung?
Es importiert
opcode.py, um vor Umbenennungen zu schützen. Außerdem verwendet esLOAD_CONSTundLOAD_GLOBAL, die fundamental sind und schon immer existieren. Nichtsdestotrotz könnte sich das Codierungsschema ändern und diese Implementierung müsste sich zusammen mit Modulen wiedisändern, die ebenfalls auf dem aktuellen Codierungsschema basieren. - Was ist die Auswirkung auf die Startzeit?
Ich konnte keinen Unterschied messen. Keine der Startmodule wird gebunden, außer warnings.py. Außerdem ist die Bindungsfunktion sehr schnell und durchläuft den Code-String nur einmal auf der Suche nach dem
LOAD_GLOBALOpcode.
Beispielimplementierung
Hier ist eine Beispielimplementierung für codetweaks.py
from types import ClassType, FunctionType
from opcode import opmap, HAVE_ARGUMENT, EXTENDED_ARG
LOAD_GLOBAL, LOAD_CONST = opmap['LOAD_GLOBAL'], opmap['LOAD_CONST']
ABORT_CODES = (EXTENDED_ARG, opmap['STORE_GLOBAL'])
def bind_constants(f, builtin_only=False, stoplist=[], verbose=False):
""" Return a new function with optimized global references.
Replaces global references with their currently defined values.
If not defined, the dynamic (runtime) global lookup is left undisturbed.
If builtin_only is True, then only builtins are optimized.
Variable names in the stoplist are also left undisturbed.
If verbose is True, prints each substitution as is occurs.
"""
import __builtin__
env = vars(__builtin__).copy()
stoplist = dict.fromkeys(stoplist)
if builtin_only:
stoplist.update(f.func_globals)
else:
env.update(f.func_globals)
co = f.func_code
newcode = map(ord, co.co_code)
newconsts = list(co.co_consts)
codelen = len(newcode)
i = 0
while i < codelen:
opcode = newcode[i]
if opcode in ABORT_CODES:
return f # for simplicity, only optimize common cases
if opcode == LOAD_GLOBAL:
oparg = newcode[i+1] + (newcode[i+2] << 8)
name = co.co_names[oparg]
if name in env and name not in stoplist:
value = env[name]
try:
pos = newconsts.index(value)
except ValueError:
pos = len(newconsts)
newconsts.append(value)
newcode[i] = LOAD_CONST
newcode[i+1] = pos & 0xFF
newcode[i+2] = pos >> 8
if verbose:
print name, '-->', value
i += 1
if opcode >= HAVE_ARGUMENT:
i += 2
codestr = ''.join(map(chr, newcode))
codeobj = type(co)(co.co_argcount, co.co_nlocals, co.co_stacksize,
co.co_flags, codestr, tuple(newconsts), co.co_names,
co.co_varnames, co.co_filename, co.co_name,
co.co_firstlineno, co.co_lnotab, co.co_freevars,
co.co_cellvars)
return type(f)(codeobj, f.func_globals, f.func_name, f.func_defaults,
f.func_closure)
def bind_all(mc, builtin_only=False, stoplist=[], verbose=False):
"""Recursively apply bind_constants() to functions in a module or class.
Use as the last line of the module (after everything is defined, but
before test code).
In modules that need modifiable globals, set builtin_only to True.
"""
for k, v in vars(mc).items():
if type(v) is FunctionType:
newv = bind_constants(v, builtin_only, stoplist, verbose)
setattr(mc, k, newv)
elif type(v) in (type, ClassType):
bind_all(v, builtin_only, stoplist, verbose)
def f(): pass
try:
f.func_code.code
except AttributeError: # detect non-CPython environments
bind_all = lambda *args, **kwds: 0
del f
import sys
bind_all(sys.modules[__name__]) # Optimizer, optimize thyself!
Beachten Sie die automatische Erkennung einer Nicht-CPython-Umgebung, die keine Bytecodes hat [2]. In dieser Situation würden die Bindungsfunktionen einfach die ursprüngliche Funktion unverändert zurückgeben. Dies stellt sicher, dass die Zwei-Zeilen-Zusätze zu Bibliotheksmodulen keine Auswirkungen auf andere Implementierungen haben.
Der endgültige Code sollte eine Flagge hinzufügen, um die Bindung einfach deaktivieren zu können.
Referenzen
[1] ASPN-Rezept für eine nicht-private Implementierung https://code.activestate.com/recipes/277940/
Urheberrecht
Dieses Dokument wurde gemeinfrei erklärt.
Quelle: https://github.com/python/peps/blob/main/peps/pep-0329.rst
Zuletzt geändert: 2025-02-01 08:59:27 GMT