3 Punkte von GN⁺ 2025-03-11 | 1 Kommentare | Auf WhatsApp teilen
  • Das CPython-Projekt hat kürzlich eine neue Implementierungsstrategie für den Bytecode-Interpreter eingeführt. Erste Ergebnisse zeigten auf verschiedenen Plattformen im Durchschnitt eine Leistungssteigerung von 10–15 %.
  • Diese Leistungssteigerung war jedoch hauptsächlich das Ergebnis der Umgehung eines Regressionsproblems in LLVM 19. Im Vergleich mit besseren Referenzen (z. B. GCC, clang-18, LLVM 19 mit bestimmten Tuning-Flags) schrumpfte der Leistungsgewinn auf 1–5 %.

Leistungsergebnisse

  • Mehrere Builds des CPython-Interpreters wurden mit verschiedenen Compilern und Konfigurationsoptionen benchmarked. Getestet wurde auf einem Intel-Server und einem Apple M1 MacBook Air.
  • Alle Builds verwendeten LTO und PGO. Als Referenz wurde clang18 verwendet, zusammen mit dem in pypeformance/pyperf compare_to gemeldeten Durchschnitt.
  • Vergleich der Compiler-Leistung
    • Apple M1 MacBook Air:
      • clang18: Referenz
      • clang19: 1,12× langsamer
      • clang19.taildup: 1,02× langsamer
      • clang19.tc: 1,00× langsamer
      • gcc: N/A
  • Der Tail-Call-Interpreter zeigte im Vergleich zu clang-18 weiterhin einen Geschwindigkeitsvorteil, aber der Leistungsabfall beim Wechsel zu clang-19 war deutlich ausgeprägter.

LLVM-Regression

Kurzer Hintergrund

  • Traditionelle Bytecode-Interpreter bestehen aus einer switch-Anweisung innerhalb einer while-Schleife. Die meisten Compiler kompilieren switch als Sprungtabelle.
  • Moderne C-Compiler unterstützen das Muster, die Adresse von Labels zu nehmen und sie als „computed goto“ zu verwenden. CPython nutzte dieses Muster bis zu den Tail-Call-Arbeiten.

LLVM-19-Regression

  • LLVM 19 schränkte den Tail-Duplication-Pass ein, sodass die Duplizierung beendet wird, wenn die IR-Größe einen bestimmten Grenzwert überschreitet. Dadurch wurden in CPython alle Dispatch-Sprünge zusammengeführt, was den Zweck der auf berechneten goto basierenden Implementierung vollständig zunichtemachte.

Weitere Auffälligkeiten

  • Es ist zwar sicher, dass die Änderung an der Logik zur Tail-Call-Duplizierung die Regression verursacht hat, aber das Ausmaß der Regression lässt sich damit nicht vollständig erklären.
  • Auf modernen Prozessoren sind Geschwindigkeitsgewinne von 2–4 % üblicher.

Ist computed goto notwendig?

  • Der Benchmark clang19.nocg behauptet, schneller als clang19 zu sein. Das zeigt, dass der Compiler mit einem switch-basierten Interpreter dieselben Optimierungen durchführen kann.

Fix

  • LLVM Pull Request 114990 hat die Regression behoben. Dieser Fix stellt die erwartete Leistung wieder her.

Rückblick

Über Benchmarking

  • Bei der Optimierung von Systemen werden Benchmarks und Benchmarking-Methoden erstellt, um vorgeschlagene Änderungen zu bewerten.
  • Benchmarks erfordern zusätzliche Annahmen und Überzeugungen, um bestimmte Datenpunkte zu verallgemeinern.

Referenzwert

  • Wenn eine neue Lösung oder Methode vorgeschlagen wird, ist es üblich, sie mit dem „aktuell besten bekannten Ansatz“ zu vergleichen.

Über Software Engineering

  • Softwaresysteme sind komplex, miteinander verbunden und verändern sich schnell.
  • Optimierende Compiler stehen in einem Spannungsverhältnis dazu, den Code zu optimieren und zugleich die Absicht des Programmierers zu respektieren.

Optimierende Compiler

  • Das Attribut musttail steht für eine neue Art compilerbezogener Funktion im Zusammenhang mit Optimierung. Es könnte einen leistungsfähigeren Stil zum Schreiben performancekritischen Codes ermöglichen.

Noch ein Wort zu nix

  • nix war in diesem Projekt äußerst nützlich. Es hat stark dabei geholfen, mehrere Versionen des Python-Interpreters zu verwalten und zu bauen.

1 Kommentare

 
GN⁺ 2025-03-11
Hacker-News-Kommentare
  • Hallo. Ich bin der Autor des PRs, der einen Tail-Calling-Interpreter in CPython eingeführt hat

    • Zuerst möchte ich Nelson danken, der fast einen Monat damit verbracht hat, die eigentliche Ursache dieses Problems zu finden
    • Außerdem schäme ich mich sehr und es tut mir leid, dass ich einen so großen Fehler gemacht habe
    • Auch das CPython-Team hatte nicht erwartet, dass der von uns verwendete Compiler einen solchen Bug haben würde
    • Ich habe den Entschuldigungs-Blogpost hier veröffentlicht: Link
  • Benchmarking ist wirklich eine Aufgabe, die sehr schwer gut zu machen ist

    • Kürzlich habe ich eine Methode gefunden, einen Algorithmus etwa 15 % schneller zu machen
    • Während der Tests wurde jedoch der ursprüngliche Code um 15 % schneller, ohne überhaupt die schnellere Version der Funktion aufzurufen
    • Das lag an Code- und Speicher-Layout-Problemen, weil die Ausrichtung zum CPU-Cache besser passte
    • Casey Muratori macht dazu gerade eine interessante Serie
  • Ich zolle dem Autor Anerkennung dafür, dass er die Wahrheit hinter diesem Problem aufgedeckt hat

    • Der Tail-Call-Interpreter in Python 3.14 ist weiterhin eine gute Verbesserung
    • Dieser Vorfall hat gezeigt, wie wichtig strenges Benchmarking und Tests in unterschiedlichen Umgebungen sind
    • Außerdem wurde dadurch nun ein Compiler-Bug gefunden, von dem letztlich alle profitieren können
    • Ich frage mich, wie viele Ergebnisse à la „X % schneller“ in Wirklichkeit auf Benchmarking-Artefakte oder unbekannte Regressionen zurückgehen
  • Das ist ein gutes Beispiel dafür, dass C keine „maschinennahe“ Sprache ist

    • clang-19 kompiliert einen Interpreter mit computed goto zwar „korrekt“, erzeugt dabei aber Ausgabe, die völlig von der beabsichtigten Optimierung abweicht
    • Auch andere Compiler-Versionen wenden Optimierungen auf einen „naiven“ switch()-basierten Interpreter an
  • Durch Anpassungen daran, wie der Compiler die Schleife organisiert, ist der Tail-Call-Interpreter nicht so effektiv wie angekündigt

    • CPU-Architektur und Version sind äußerst wichtig
    • Die abstrakte C-Maschine ist nicht Low-Level genug, um die Absicht angemessen auszudrücken
    • Einige besonders paranoide Interpreter-Implementierungen gehen deshalb wieder dazu über, direkt Assembler zu schreiben
    • luajit implementiert ein Makrosystem, das effiziente Assembler-Schleifenimplementierungen architekturübergreifend portabel macht
  • Die Performance eines Python-Builds zu bewerten, ist sehr schwierig

    • Kürzlich hat das astral-Team gezeigt, dass conda-forge-Builds schneller sind als die meisten anderen Builds
    • Ich frage mich, wie der Tail-Call-Interpreter zusammen mit anderen Build-Optimierungen funktioniert
  • Verwandte Diskussionen:

  • Ein großartiger Artikel

    • In einem der verlinkten Artikel wird erwähnt, dass 3.14.0a5 1,12-mal schneller als 3.13 sei
    • Ich bin verwirrt, ob der Benchmark ausgeführt wurde, während andere Prozesse das System zusätzlich belasteten
    • Benchmarks sollten in einer streng kontrollierten Umgebung durchgeführt werden, um externe Variablen auszuschließen
  • Ich habe vor Kurzem Benchmarks von Python 3.9 bis 3.13 durchgeführt

    • Bis 3.11 gab es Performance-Verbesserungen, aber 3.12 und 3.13 waren etwa 10 % langsamer als 3.11
    • Ich dachte, mein eigener Benchmark sei vielleicht nicht ausreichend, aber beim Deployment in einen Kerndienst habe ich dieselbe Veränderung beobachtet
  • Ich frage mich, wie diese Optimierung mit Tail-Call-Optimierung zusammenhängt

    • Die Implementierung der Jump-Table des Interpreters sollte die Erzeugung von Stack-Frames nicht beeinflussen