- Der neue Tail-Call-Interpreter von CPython sah zunächst nach einer durchschnittlichen Verbesserung von 10–15 % in pyperformance aus; nach Anpassung der Baseline schrumpft der tatsächliche Gewinn je nach Konfiguration jedoch auf etwa 1–5 %
- Die großen Verbesserungen waren weniger ein Effekt der neuen Implementierung selbst, sondern eher das Ergebnis einer Umgehung einer LLVM-19-Regression; der zentrale Faktor war, dass Clang 19 den bisherigen computed-goto-Dispatch nicht korrekt replizierte
- Auf einem Intel Raptor Lake i5-13500 war der clang19-Build 1,09-mal langsamer als clang18, während clang19.tc 1,03-mal schneller war; auf einem Apple M1 MacBook Air war clang19 1,12-mal langsamer und clang19.tc blieb bei 1,00-mal langsamer
- Durch die Tail-Duplication-Begrenzung in LLVM sank die Zahl indirekter Sprünge von 332 bei clang18 auf 3 bei clang19, wodurch die beabsichtigte Struktur des computed-goto-basierten Interpreters praktisch verschwand
- Der Tail-Call-Ansatz bleibt eine sinnvolle Verbesserung, doch ein Ansatz, der Compiler-Optimierungen wie
musttailexplizit verlangt, kann für performancekritischen Code robuster sein
Baseline-Effekt, der wie eine Performance-Verbesserung aussah
- Das CPython-Projekt hat vor etwa einem Monat eine neue Implementierungsstrategie für den Bytecode-Interpreter gemergt
- Die ersten Ergebnisse zeigten über verschiedene Plattformen und pyperformance-Benchmarks hinweg im Schnitt 10–15 % Performance-Gewinn
- Spätere Analysen zeigten, dass diese große Verbesserung hauptsächlich das Ergebnis einer zufälligen Umgehung einer LLVM-19-Regression war
- Im Vergleich zu GCC, clang-18 oder LLVM 19 mit bestimmten Tuning-Flags sinkt der Gewinn auf etwa 1–5 %
- Der Tail-Call-Interpreter selbst erzeugt tatsächlich einen Geschwindigkeitsgewinn, aber der Effekt ist moderater, als die ersten Zahlen nahelegten
- Wer mit clang-19 oder einer späteren Version gebaut hat, hatte möglicherweise einen bisherigen Pfad, der tatsächlich 10–15 % langsamer war
- Simon Willison reproduzierte einen Geschwindigkeitsgewinn von 10 % beim Vergleich von
python-build-standalone-Builds mit Python 3.13
- Simon Willison reproduzierte einen Geschwindigkeitsgewinn von 10 % beim Vergleich von
Benchmark-Aufbau und zentrale Zahlen
- Mehrere CPython-Builds wurden auf einem Intel-Server und einem Apple M1 MacBook Air verglichen
- Der Intel-Server ist ein bei Hetzner betriebener Raptor Lake i5-13500
- Alle Builds verwenden LTO und PGO
- Zur Reproduktion der Builds wurde eine nix-Konfiguration verwendet
- Verglichen wurden die folgenden Varianten
clang18: Clang 18.1.8, computed gotogcc: GCC 14.2.1, computed goto, nur Intelclang19: Clang 19.1.7, computed gotoclang19.tc: Clang 19.1.7, neuer Tail-Call-Interpreterclang19.taildup: Clang 19.1.7, computed goto plus-mllvm-Tuning-Flag zur Umgehung der Regression
- Die durchschnittlichen pyperformance-Ergebnisse mit
clang18als Baseline sind:- Raptor Lake i5-13500:
clang19: 1,09-mal langsamerclang19.taildup: 1,01-mal schnellerclang19.tc: 1,03-mal schnellergcc: 1,02-mal schneller
- Apple M1 MacBook Air:
clang19: 1,12-mal langsamerclang19.taildup: 1,02-mal langsamerclang19.tc: 1,00-mal langsamer
- Raptor Lake i5-13500:
- Der Tail-Call-Interpreter zeigte gegenüber clang-18 gewisse Geschwindigkeitsgewinne, blieb aber kleiner als der Performance-Einbruch, der durch den Wechsel zu clang-19 entstand
clang18.tckonnte nicht gemessen werden- Der Grund ist, dass der Tail-Call-Interpreter von einer neu in Clang 19 hinzugekommenen Compiler-Funktion abhängt
- Wegen dieser Einschränkung waren mehr Benchmark-Kombinationen nötig, um die Situation zu verstehen
Der durch die LLVM-19-Regression verursachte Dispatch-Zusammenbruch
- Ein traditioneller Bytecode-Interpreter verarbeitet Opcodes über eine
switch-Anweisung innerhalb einerwhile-Schleife- Der Compiler kompiliert
switchüblicherweise zu einer Sprungtabelle und indirekten Sprüngen
- Der Compiler kompiliert
- Es ist seit Langem bekannt, dass solche Interpreter schneller werden können, wenn man die Dispatch-Logik in den Körper jedes Opcodes repliziert
- Statt am Ende eines Opcodes zum Schleifenanfang zurückzukehren, gibt es die Logik zum Dekodieren der nächsten Instruktion und zum Indizieren der Sprungtabelle separat pro Opcode
- C-Compiler stellen eine Funktion bereit, mit der sich Adressen von Labels nehmen und als computed goto verwenden lassen; auch CPython nutzte vor der Tail-Call-Arbeit eine Interpreter-Schleife dieser Art
- Clang/LLVM fasst aus Gründen der Compiler-Performance mehrere
gotos eines computed goto intern zu einer einzelnen indirectbr LLVM instruction zusammen- Anschließend repliziert die Codegenerierung per Tail Duplication die Verzweigungslogik wieder an die jeweiligen Stellen
- Dieser Ablauf ist in einem älteren LLVM-Blogbeitrag auf hoher Ebene dokumentiert
- LLVM 19 führte eine Begrenzung des Tail-Duplication-Passes ein, um in bestimmten Fällen stark erhöhte Compile-Zeit oder Speichernutzung zu vermeiden
- In CPython führte diese Begrenzung dazu, dass Clang die Dispatch-Sprünge in zusammengeführter Form beließ
- Dadurch wurde der Zweck der computed-goto-basierten Implementierung praktisch ausgehebelt
- Dieses Problem wurde zuerst bei anderen Sprachimplementierungen mit ähnlichen Interpreter-Schleifen identifiziert, aber dass es CPython betrifft, war nicht bekannt
- Disassembliert man den Objektcode und zählt die indirekten Sprünge, ist der Unterschied direkt sichtbar
_PyEval_EvalFrameDefaultim clang18-Build: 332jmp *_PyEval_EvalFrameDefaultim clang19-Build: 3jmp *
Die unklare Rolle von computed goto
- Dass die Änderung an der Tail-Duplication-Logik die Regression verursachte, wird dadurch bestätigt, dass die Performance nach dem Fix wieder auf clang-18-Niveau zurückkehrt
- Allerdings ist die Größe der Regression damit nicht vollständig erklärt
- Historisch wurde die Replikation des Opcode-Dispatchs als Verbesserung genannt, die Interpreter um 20 % bis 100 % schneller machen könne
- Auf modernen Prozessoren mit verbesserten Branch Predictors zeigen neuere Arbeiten dagegen kleinere Verbesserungen von etwa 2–4 %
- Python unterstützt per Konfigurationsoption auch einen alten Interpreter mit einer einzelnen
switch-Anweisungclang18.nocg: 1,01-mal schneller als clang18clang19.nocg: 1,02-mal langsamer als clang18clang19: 1,09-mal langsamer als clang18
- Dass
clang19.nocgschneller alsclang19ausfällt, ist eine zusätzliche Überraschung- Clang 18 oder Clang 19 mit passenden Flags repliziert die Dispatch-Logik auch bei einem
switch-basierten Interpreter in jeden Opcode-Körper
- Clang 18 oder Clang 19 mit passenden Flags repliziert die Dispatch-Logik auch bei einem
- Auch der Vergleich der Zahl indirekter Sprünge zeigt diesen Unterschied
clang18: 332clang18.nocg: 306clang19.nocg: 3clang19: 3
- In modernen Clang-Versionen könnte der gesamte computed-goto-Interpreter unnötige Komplexität sein
- Denn der Compiler kann dieselbe Transformation auch bei
switch-basiertem Code durchführen - Umgekehrt reichte computed goto selbst nicht aus, um die Transformation zu garantieren
- Denn der Compiler kann dieselbe Transformation auch bei
- GCC 14.2.1 replizierte
switchnicht, implementierte bei Verwendung von computed goto aber das beabsichtigte Verhalten
Fixes und Workarounds
- LLVM Pull Request 114990 wurde kurz nach Veröffentlichung des Beitrags gemergt und behebt die Regression
- Bereits Benchmarks vor dem Merge bestätigten, dass der Fix die erwartete Performance wiederherstellt
- In Releases vor dem Fix lässt sich der Abbruchschwellwert für Tail Duplication über eine Tuning-Option anpassen, die durch den regressionsverursachenden PR hinzugefügt wurde
- Setzt man diese Begrenzung in clang-19 auf einen sehr hohen Wert, kann ein ähnliches Verhalten wiederhergestellt werden
- Bei LTO-Builds ist die Weitergabe dieser Option kompliziert
- Tail Duplication findet während der Codegenerierung statt, und die Codegenerierung eines LTO-Builds geschieht nicht zur Compile-Zeit, sondern zur Link-Zeit
- Deshalb muss das Flag nicht nur an den Compiler, sondern auch an
lldübergeben werden
- Das verwendete Konfigurationsbeispiel übergibt in der
./configure-Phase-mllvm -tail-dup-pred-size=5000anOPTundLDFLAGS
Das Baseline-Problem, das Benchmarks sichtbar machten
- Benchmarks können Performance-Unterschiede zwischen bestimmten Builds genau messen, doch um daraus eine „allgemeine Performance-Verbesserung“ abzuleiten, braucht es zusätzliche Annahmen
- Die Benchmarks des Tail-Call-Interpreters zeigten gegenüber dem bisherigen computed-goto-Interpreter 10–15 % bessere Ergebnisse, aber für eine Verallgemeinerung auf breitere Schlussfolgerungen war die Baseline komplex
- Bei Performance-Arbeit ist womit man vergleicht ein immer wieder schwieriges Problem
- Selbst wenn man den theoretisch besten bekannten Ansatz versteht, ist es eine eigene Herausforderung, reale Betriebssysteme, Compiler-Optionen und Flags korrekt abzustimmen
- Öffentliche Benchmarks auf alter Hardware oder in schwer reproduzierbaren Größenordnungen eignen sich möglicherweise nicht für direkte Vergleiche
- Auch in Machine-Learning-Papers ist bei behaupteten Algorithmusverbesserungen oft zuerst die Frage wichtig, „mit welcher Baseline wurde verglichen?“, noch vor „was wurde gemacht?“
- Vergleicht man mit einer schlecht getunten Baseline, lassen sich leicht beeindruckende Ergebnisse erzielen
Optimierende Compiler und musttail
- Der computed-goto-Fall zeigt, dass Erwartungen an optimierende Compiler miteinander kollidieren können
- Der Compiler soll die Absicht des Programmierers respektieren und dasselbe Verhalten bewahren
- Gleichzeitig soll er komplexe und nicht intuitive Transformationen durchführen, um Code schneller zu machen
- clang-19 kompilierte den computed-goto-Interpreter aus Sicht des Programmverhaltens korrekt, erzeugte aber Output, der der Optimierungsabsicht völlig entgegenlief
- Andere Clang-Versionen wenden dieselbe beabsichtigte Optimierung sogar auf einen einfachen
switch()-basierten Interpreter an - Computed goto auf Quellcode-Ebene und Dispatch-Replikation auf Maschinencode-Ebene erscheinen fast wie orthogonale Konzepte
- Da das Ausführungsergebnis gleich ist, können aktuelle Werkzeuge diesen Unterschied schwer konsistent ausdrücken
- Der Tail-Call-Interpreter basiert auf dem musttail attribute
musttailändert zwar nicht das traditionell beobachtbare Programmverhalten, ist aber eher ein Dialog mit dem Optimizer- Der Compiler muss eine bestimmte Optimierung durchführen können; wenn diese Optimierung nicht möglich ist, soll die Kompilierung fehlschlagen
- Dieser Stil kann eine robustere Art sein, performancekritischen Code zu schreiben, auch während sich Compiler weiterentwickeln
- Es wäre prüfenswert, ob ein hypothetisches Attribut wie
[[clang::musttailduplicate]]computed goto in der Interpreter-while-Schleife ersetzen könnte
Reproduzierbarkeit und Grenzen durch nix
nixwar sehr hilfreich, um mehrere Python-Interpreter-Builds zu verwalten- Im Verlauf der Experimente wurden Dutzende Python-Interpreter mit den vier Compilern
gcc,clang-18,clang-19,clang-20und verschiedenen Flag-Kombinationen gebaut und gebenchmarkt
- Im Verlauf der Experimente wurden Dutzende Python-Interpreter mit den vier Compilern
- Mit
nixließen sich parallele Versionen reproduzierbar und isoliert halten- Es war klar nachvollziehbar, welcher Build von welchem Compiler und welchen Flags stammte
- Auch die Definition der Build-Matrix ließ sich mit kurzen Abstraktionen verwalten
- Einen Custom-LLVM mit angewendetem Bugfix-Patch zu bauen und damit anschließend Python zu bauen, war ebenfalls mit etwa 10 Zeilen Code möglich
- Es bleiben auch Nachteile
nixunterscheidet sich in Teilen von der üblichen Softwarenutzung, sodass sich nicht völlig ausschließen lässt, dass solche Unterschiede Benchmarks oder Schlussfolgerungen beeinflusst haben- Zum Beispiel baut nix Projekte standardmäßig mit bestimmten Hardening-Flags; früh wurde entdeckt, dass diese Flags den Tail-Call-Interpreter unverhältnismäßig beeinflussen
- Nix ist stark in Skalierbarkeit und Anpassbarkeit, aber um eine bestimmte Anpassungsmethode zu finden, waren viel Trial-and-Error und das Durchsuchen der
nixpkgs-Quellen nötig
1 Kommentare
Meinungen auf Hacker News
Ich bin der Autor des PRs, der den Tail-Call-Interpreter in CPython eingebracht hat.
Zunächst danke an Nelson, der fast einen Monat damit verbracht hat, die Ursache dieses Problems zu finden.
Dass der für die Baseline verwendete Compiler so einen Bug haben würde, hatte weder ich noch vermutlich das CPython-Team erwartet. Ich habe einen großen Fehler gemacht und schäme mich sehr dafür; es tut mir leid.
Ich habe auch einen Entschuldigungsbeitrag veröffentlicht: https://fidget-spinner.github.io/posts/apology-tail-call.htm...
Es wurde eine Performance-Steigerung von 10–15 % angekündigt, aber mit einem bugfreien Compiler liegt sie eher bei 1–5 %. Und selbst diese Zahl war nicht völlig falsch, sondern traf nur unter bestimmten Bedingungen zu.
Die Verbesserung wurde entwickelt, gemessen und der PR wurde reviewt – damit wurde die eigentliche Arbeit erledigt. Dass die Zahlen wegen eines Problems mit der verwendeten clang-Version missverständlich waren, wirkt wie ein nachvollziehbarer Fehler, in den jeder geraten kann.
Trotzdem wurde eine sinnvolle Performance-Verbesserung erzielt und außerdem eine Compiler-Regression gefunden; dagegen wirken die falschen Zahlen eher klein. Ich weiß auch nicht recht, wer dadurch tatsächlich geschädigt wurde, und es scheint mir nichts zu sein, wofür man sich entschuldigen müsste.
Wenn bei einem so alten System wie dem Python-Interpreter 3–5 % erhalten bleiben, ist das für sich genommen schon ein großer Erfolg, auf den man durchaus stolz sein kann.
Nach ungefähr 30 Jahren wird man bei bedeutenden Performance-Verbesserungen in lange bestehenden Systemen, insbesondere bei Verbesserungen über 1 %, erst einmal skeptisch.
Reale Verbesserungen gibt es zwar, aber sie sind selten; oft wurde die Zeit lediglich an eine andere Stelle verschoben, sodass sie im Benchmark nicht auftaucht. Außerdem laufen Benchmarks in kontrollierten Umgebungen, um Effekte zu isolieren, während reale Software auf VMs oder Desktops zusammen mit allerlei anderen Dingen läuft.
Ich habe oft gesehen, dass Verbesserungen, die in einer isolierten Umgebung eindeutig groß wirkten, in der Produktionsumgebung verschwanden oder sogar negativ wurden.
CPython hat es noch schwerer, weil es viele Umgebungen unterstützen muss, und es gibt kein einzelnes Produktionsziel, bei dem man sagen könnte: „Wenn es in Produktion nicht schneller ist, ist es nicht wirklich schneller.“ In so einer Welt Performance zu verbessern, ist wirklich schwierig.
Letztlich sind Performance-Tuning und Messung sehr schwierig, und das Einzige, wofür man sich vielleicht entschuldigen könnte, ist, diese Tatsache gelernt zu haben.
Ich hoffe, ihr habt keine Angst davor, falschzuliegen. Jeder liegt ohnehin mal falsch. Man muss es so machen wie hier: sagen „Das haben wir wohl vermasselt“, und dann herausfinden, wie man damit umgeht und es künftig vermeidet.
[1] Das ist nicht nur bei Performance so, sondern auch bei menschlichen Prozessen häufig. Wenn zum Beispiel ein Team für Code-Review-Tools sagt: „Wir haben die Code-Review-Zeit um 15 % reduziert und damit den Workflow aller beschleunigt“, kann es in Wirklichkeit Arbeit an anderer Stelle im System erzeugt haben, ohne den Gesamtfluss zu beschleunigen; die 15 % wurden nur an eine nicht gemessene Stelle verschoben.
Theoretisch sollten dem Compiler bei einem solchen Kontrollflussgraphen und Profil genügend Informationen vorliegen, um für einen traditionellen
switch()-basierten Interpreter optimalen Code zu erzeugen. In der Praxis kämpft man jedoch mit dem Compiler, wenn eine Funktion so groß und verwoben ist.Er spillt wichtige Variablen, die man in Registern halten möchte, zieht Stack-Frame-Manipulationen nach oben, die man rund um Fallback-Funktionsaufrufe verkleinern wollte, und führt wegen Branch Prediction identische Codepfade zusammen, die man eigentlich trennen wollte. Es kann sich anfühlen, als würde man mit Handschuhen Klavier spielen.
Genau diese „Zusammenführung identischer Codepfade“ ist auch hier passiert, und der „fehlerhafte“ Compiler hat identische Pfade zusammengeführt und dadurch die Performance verschlechtert.
Der „korrigierte“ Compiler tut das nicht mehr, aber auch dieser Fix ist letztlich eher eine Anpassung interner Compiler-Heuristiken. Es gibt keine Garantie, dass dieser oder andere Compiler ihre Heuristiken auch künftig in einer für uns günstigen Weise beibehalten.
Ein Tail-Call-Interpreter hingegen kann das gewünschte Maschinen-Code-Muster im Interpreter selbst ausdrücken. Wenn man die Attribute
musttail,noinlineundpreserve_nonezusammen verwendet, kann man das Problem so einschränken, dass man deutlich weniger von Optimierer-Heuristiken abhängig ist.Deshalb gehen die Vorteile des Tail-Call-Interpreters über eine bloße Performance-Steigerung von 3–5 % hinaus; bei manchen Compilern kann es eine noch größere verlässliche Performance-Verbesserung sein.
Könnte man die Benchmarks verbessern, um ähnliche Fälle zu verhindern?
[0] https://github.com/faster-cpython/benchmarking-public
Benchmarking wirklich korrekt zu machen, ist wahnsinnig schwierig. Es gibt zu viele Faktoren, die einen täuschen können.
Kürzlich dachte ich, ich hätte eine Methode gefunden, einen Algorithmus um etwa 15 % schneller zu machen. Zumindest sagten das alle Benchmarks.
Dann kopierte ich die schnellere Funktion in das Test-Harness, rief sie aber tatsächlich gar nicht auf und rief nur weiter die ursprüngliche, langsamere Version auf – und trotzdem war es noch immer 15 % schneller. Nicht einmal ausgeführter Code hatte also den ursprünglichen Code schneller gemacht.
Natürlich lag es an Code- und Speicherlayout; irgendetwas war verschoben worden und passte dadurch besser in den CPU-Cache.
Es ist wirklich schwer zu erkennen, ob ein Geschwindigkeitsgewinn daher kommt, dass der Code tatsächlich „besser“ geworden ist, oder ob man irgendwo einfach Glück mit besserer Ausrichtung hatte.
Casey Muratori schreibt auf Substack eine sehr interessante Serie zu diesem Thema.
Respekt an den Autor dafür, den tatsächlichen Hintergründen so gründlich nachgegangen zu sein. Der Tail-Call-Interpreter in Python 3.14 ist weiterhin eine gute Verbesserung, und ein paar Prozent Leistungsgewinn in einer Sprach-Runtime sind hart erarbeitet.
Nur war es eben kein magisches kostenloses 15-%-Mittagessen.
Wichtiger ist, dass der Fall sehr gut zeigt, wie wichtig sauberes Benchmarking und Tests in mehreren Umgebungen sind. Er hat außerdem einen Compiler-Bug offengelegt, der allen zugutekommen kann.
Das ist genau die Art von Tiefenanalyse, die einen dazu bringt, die nächste große Performance-Behauptung noch einmal zu prüfen. Eine weiterführende Frage ist, wie viele der vielen derzeit kursierenden „X % schneller“-Ergebnisse in Wahrheit Benchmark-Artefakte oder unbekannte Regressionen sind.
Wie können wir solche Fallen künftig besser vermeiden?
Benchmarken sie den Compiler selbst nicht? Haben bestehende Benchmarks auf Compiler- oder Python-Seite diesen Compiler nicht verwendet?
Das ist ein gutes Beispiel dafür, wie wenig zutreffend Aussagen sind, C sei „nah an der Maschine“ oder „portable Assembly“. Moderne Optimierer verändern Logik sehr aggressiv, wenn es keine beobachtbaren Effekte gibt.
Im Artikel hieß es auch: „clang-19 kompiliert den computed-goto-Interpreter in dem Sinne ‚korrekt‘, dass das resultierende Binary alle erwarteten Werte liefert, zugleich steht diese Ausgabe aber völlig im Widerspruch zur Optimierungsabsicht. Außerdem wenden andere Compiler-Versionen Optimierungen auf den ‚naiven‘
switch()-basierten Interpreter an und implementieren damit exakt dieselbe Optimierung, die wir durch Umschreiben des Quellcodes ‚beabsichtigt‘ hatten.“Bei Cs
a += 1kann man darauf vertrauen, dass ein Zahlenwert erhöht wird, während derselbe Ausdruck in C++ Speicher allozieren, den Call-Stack abwickeln oder unbekannte Dinge tun kann. Ebenso ista = "a"in C eine einfache Pointer-Zuweisung, während in C++ Speicherallokation und Ähnliches passieren kann.Die Formulierung „C ist portable Assembly“ bedeutet nicht, dass jede Anweisung direkt in äquivalenten Maschinencode kompiliert wird.
Es ist nicht überraschend, dass der Compiler durch Eingriffe in die Schleifenkonstruktion den gesamten Tail-Call-Interpreter weniger wirksam gemacht hat, als angekündigt.
Paranoide Interpreter-Implementierungen schreiben dafür wieder direkt Assembly. LuaJIT ist bekannt dafür, ein Makrosystem implementiert zu haben, um eine sehr effiziente Assembly-Loop-Implementierung architekturübergreifend portabel zu machen. Genau deshalb macht es auch Spaß, an solchen Dingen herumzuschrauben.
Vor ein paar Jahren habe ich außerdem einen Artikel und Tests zu beliebten Arten der Implementierung von Interpreter-Loops erstellt:
https://github.com/vkazanov/bytecode-interpreters-post
Moderne Branch Predictors können selbst einen einzelnen indirekten Sprung nahezu korrekt vorhersagen, wenn der Ausführungsabschnitt lang genug ist und sich das Verhalten des interpretierten Codes selbst stabil verhält.
Es gibt ein Paper, das das sowohl auf echter Hardware als auch mit bestimmten simulierten Branch Predictors untersucht hat: https://inria.hal.science/hal-01100647/document
Die Experimente, die ich in diesem Projekt gemacht habe, stützen anekdotisch dieselbe Schlussfolgerung. Ich konnte es nicht in den Artikel aufnehmen, aber ich habe mir einige Interpreter mit Hardware-CPU-Countern und
perf statangesehen, und Branch-Mispredictions traten nicht als dominierender Faktor hervor.Die Performance von Python-Builds zu bewerten, ist extrem schwierig, weil es so viele Build-Techniken gibt, die die Leistung verbessern können.
Kürzlich hatte auch das astral-Team mit diesem Problem zu tun, als sich zeigte, dass conda-forge-Builds deutlich schneller sind als die meisten anderen:
https://github.com/astral-sh/python-build-standalone/pull/54...
Ich frage mich, wie sich der Tail-Call-Interpreter verhält, wenn man ihn zusammen mit anderen bestehenden Build-Optimierungen verwendet.
Der Autor probiert mit einem genetischen Algorithmus Kombinationen aus verschiedenen Compilern und Optimierungsflags aus.
Zugehörige Diskussionen:
https://docs.python.org/3.14/whatsnew/3.14.html#whatsnew314-... --> https://news.ycombinator.com/item?id=42999672 (66 Punkte | vor 25 Tagen | 22 Kommentare)
https://blog.reverberate.org/2025/02/10/tail-call-updates.ht... --> https://news.ycombinator.com/item?id=43076088 (124 Punkte | vor 18 Tagen | 92 Kommentare)
Guter Artikel. Ein Detail ist mir aufgefallen
In einem der verlinkten Beiträge, https://simonwillison.net/2025/Feb/13/python-3140a5/, schrieb er: „3.14.0a5 war im Benchmark 1,12-mal schneller als 3.13, auf meinem extrem überlasteten M2 MacBook Pro“
Das ist ziemlich verwirrend. Heißt das, er hat den Benchmark laufen lassen, während der Computer durch andere Prozesse überlastet war? Wären die Ergebnisse dann nicht völlig unzuverlässig?
Ich dachte, solche Benchmarks würden in einer sehr kontrollierten Umgebung durchgeführt, um externe Variablen auszuschließen
Für CPython siehe https://github.com/faster-cpython/benchmarking-public
Manche hier nennen 10 % „groß“ und 1 % „normal“, aber Optimierungen wie partielles Inlining bei doppelt rekursivem Fibonacci können die tatsächliche Arbeitsmenge und Laufzeit exponentiell reduzieren
Bei zweistelligen Argumenten sind mehr als 10-fache Verbesserungen möglich, also Tausende Prozent. Genau genommen ist es exponentiell in Bezug auf den Unterschied der Rekursionstiefe, nicht der Problemgröße [1]
Auch C-Compiler können sehr empfindlich auf Heuristiken für Code-Inlining reagieren, sodass es stark von der Codeform abhängen kann, ob diese enorme Beschleunigung tatsächlich eintritt
Ein Teil des Problems ist also, dass CPUs sehr ausgefeilt und komplex geworden sind; ein anderer Aspekt ist aber, dass auch Compiler jenseits von
-O0oder-O1ausgefeilt und komplex geworden sindDer Artikel ist gut und lesenswert, aber er ist auch eines von vielen Beispielen dafür, dass die Interaktion zweier komplexer Dinge sehr überraschende Ergebnisse liefern kann. Das gilt auch außerhalb der Informatik
Menschen neigen stark dazu, zu stark zu vereinfachen, egal wie oft diese Lektion wiederholt wird
Zusätzlich verwendet der Artikel mindestens zwei CPUs, Intel und Apple M1, sowie zwei Compiler, gcc und clang; in realen Deployment-Umgebungen kann es aber viel mehr Generationen und Implementierungen von Intel, AMD und ARM sowie andere Compiler geben. Man betrachtet also nur einen sehr kleinen Ausschnitt der Gesamtkomplexität
Wissenschaftlicher wäre es, insbesondere bei Unterschieden wie „1,01-fach“, irgendeine Form von Fehlerbalken bei den Zeitmessungen anzugeben. Die Standardabweichung des Mittelwerts oder in diesem Fall vielleicht eher die Standardabweichung des Minimums könnte besser sein [2]
Um Messfehler zu reduzieren, braucht man wahrscheinlich auch eine feste CPU-Core-Zuweisung durch das OS-Scheduling
[1] https://stackoverflow.com/questions/360748/computational-com...
[2] https://github.com/c-blake/bu/blob/main/doc/tim.md
Ich habe kürzlich Benchmarks von Python 3.9 bis 3.13 durchgeführt, und bis 3.11 wurde es kontinuierlich besser
Python 3.12 und 3.13 waren jedoch etwa 10 % langsamer als 3.11
Ich dachte, mein eigener Benchmark sei vielleicht nicht gut genug, habe es aber trotzdem in einem Kerndienst ausgerollt, und die gesammelten Metriken zeigten dieselbe Veränderung
Hat jemand dasselbe Problem erlebt?
[0]: https://github.com/python/cpython/issues/123540