- Alternative Implementierungen wie Sprach-Runtimes oder JITs können trotz besserer Performance nur begrenzt übernommen werden, weil sie Änderungen an der kanonischen Implementierung und die Erwartungen der Nutzer ständig nachverfolgen müssen
- PyPy, LuaJIT und TruffleRuby zeigten hohe Ausführungsgeschwindigkeit, doch Kompatibilitätslücken und der Aufwand, neue Features nachzuziehen, wurden zu Hürden für den realen Einsatz
- YJIT wählte nicht den Weg einer separaten Ruby-Implementierung, sondern wurde innerhalb von CRuby aufgebaut, sodass es von Anfang an 100 % kompatibel zu den CRuby-Features war, und wird bei Shopify, Discourse und GitHub eingesetzt
- Eine Wahl wie Crystal, die einer bestehenden Sprache sehr ähnelt, aber nicht kompatibel ist, kann dazu führen, dass Nutzer ständig auf Unterschiede stoßen, die „fast Ruby, aber nicht Ruby“ sind
- In Bereichen mit offenen Standards wie JSON-Parsern oder JavaScript ist die Last alternativer Implementierungen geringer, doch in Ökosystemen, in denen die kanonische Implementierung faktisch der Standard ist, braucht es andere Strategien
Die wiederkehrende Falle alternativer Implementierungen
- In der Softwarewelt passiert es immer wieder, dass Projekte, die als bessere alternative Implementierung eines bestehenden Systems starten, im Schatten der kanonischen Implementierung gefangen bleiben
- Alternative Implementierungen werden an der kanonischen Implementierung (canonical implementation) gemessen, die bei Features, Performance, Ökosystem und Nutzererwartungen als Standard gilt
- Wenn sich die kanonische Implementierung ständig weiterentwickelt, muss eine alternative Implementierung viel Energie darauf verwenden, diesen Änderungen zu folgen, statt ihre eigene Richtung zu bestimmen
- Wenn man an eine traditionell interpretierte Sprache eine JIT-Implementierung anfügt, gelangen neue Features meist schneller in den Interpreter, wodurch der Nachziehaufwand auf der JIT-Seite steigt
Das Muster bei PyPy, LuaJIT und TruffleRuby
- PyPy ist ein fortschrittlicher JIT-Compiler für Python und konnte gegenüber CPython deutliche Geschwindigkeitsgewinne erzielen, wurde in der Praxis aber nur sehr wenig genutzt
- Python ist ein bewegliches Ziel, weil regelmäßig neue CPython-Versionen und Features erscheinen
- PyPy hatte Schwierigkeiten, damit Schritt zu halten, und lag dadurch stets einige Python-Versionen zurück
- Um Python-Software mit PyPy kompatibel zu halten, muss man die nutzbaren Python-Features einschränken, und die meisten Python-Programmierer wollen sich damit nicht beschäftigen
- LuaJIT bot gegenüber der auf einem klassischen Interpreter basierenden Lua-Implementierung große Performancegewinne, erhielt viel Anerkennung und eine gewisse reale Verbreitung
- Der Schöpfer von LuaJIT, Mike Pall, gilt für viele als herausragender Programmierer
- Während die Sprache Lua weiter neue Features erhielt, fiel LuaJIT ebenfalls um mehrere Versionen zurück
- Einige Lua-Nutzer zögern deshalb, LuaJIT einzusetzen
- Lua ist für Minimalismus bekannt, doch es gab keine ernsthaften Bemühungen, neue Features zu verzögern oder sie mit Mike Pall abzustimmen
- TruffleRuby warb mit den beeindruckendsten Performancewerten unter den Ruby-JITs, seine Verbreitung blieb aber begrenzt
- Ein praktischer Grund dafür ist, dass die Warm-up-Zeit von TruffleRuby deutlich länger ist als bei CRuby
- Während CRuby fortlaufend neue Features hinzufügte, mussten die TruffleRuby-Mitwirkenden daran arbeiten, mitzuhalten
- Ruby-Nutzer betrachten CRuby als kanonische Implementierung, und eine nicht vollständig kompatible Implementierung erscheint ihnen wenig erwägenswert
Der andere Weg von YJIT
- YJIT begann als weiterer Ruby-JIT, wurde aber nicht als separate Implementierung, sondern direkt innerhalb von CRuby aufgebaut
- Diese Entscheidung brachte mehrere Design-Trade-offs mit sich, erlaubte YJIT aber von Anfang an eine 100%ige Kompatibilität mit allen CRuby-Features
- Heute ist YJIT der „offizielle“ JIT von Ruby und wird bei Shopify, Discourse und GitHub eingesetzt
- Nutzer, die github.com oder einen Shopify-Store besuchen, sind damit faktisch mit YJIT in Berührung gekommen
- Unter den Ruby-JIT-Compilern war YJIT bisher am erfolgreichsten, und Kompatibilität spielte dabei eine Schlüsselrolle
„If you can’t beat them, join them“ reicht nicht aus
- Wenn man sich selbst als alternative Implementierung positioniert, landet man mit hoher Wahrscheinlichkeit in einem dauerhaften Aufholspiel im Schatten der kanonischen Implementierung
- Wenn sich das kanonische Projekt weiterentwickelt, muss die alternative Implementierung hinterherlaufen und hat nur begrenzte Entscheidungsfreiheit über die Richtung des eigenen Projekts
- Sich der kanonischen Implementierung anzuschließen kann zu besseren Ergebnissen führen, löst aber nicht jeden Fall
- Crystal im Ruby-Ökosystem ist eine statisch kompilierte Sprache mit Ruby-ähnlicher Syntax und Typherleitung
- Crystal zielte bewusst nicht auf Ruby-Kompatibilität und spaltete sich von Ruby ab
- Für Rubyists wirkt es wie eine Sprache, die „fast Ruby, aber nicht ganz Ruby“ ist; tatsächlich gibt es viele subtile Unterschiede und Inkompatibilitäten
- Diese Ähnlichkeit untergräbt die Erwartungen der Nutzer und stiftet Verwirrung
- Crystal hätte womöglich bessere Ergebnisse erzielt, wenn es sich von Anfang an nicht als Ruby-ähnlich vermarktet hätte
Die Wahl, Konkurrenz zu vermeiden und eine eigene Richtung zu haben
- Peter Thiels Satz „competition is for losers“ wird hier im Sinn verwendet, sich nicht selbst in eine Lage unnötiger Konkurrenz zu bringen
- Daraus folgt der Rat, bei einer neuen Programmiersprache lieber nicht zu versuchen, ein Subset von Python oder etwas zu bauen, das bestehenden Sprachen an der Oberfläche zu ähnlich ist
- Wenn man etwas Eigenes schafft, kann man das System in eigenem Tempo und nach eigener Richtung weiterentwickeln, ohne an Erwartungen gebunden zu sein, bei Performance, Feature-Umfang oder Bibliotheks-Ökosystem mit einer anderen Implementierung gleichzuziehen
- Dieser Rat gilt für Situationen, in denen es für eine Sprache oder ein System eine kanonische Implementierung gibt
- Bereiche mit offenen Standards können eine Ausnahme sein
- JSON-Parser sind ein gutes Ziel für eigene Implementierungen, weil es dafür eine relativ kleine, klar definierte Spezifikation gibt, die sich nicht schnell ändert
- Bei JavaScript gibt es mehrere browserbasierte Implementierungen; möglich ist das, weil die JS-Spezifikation von einem externen Standardisierungsgremium gepflegt wird
- Die Verantwortlichen für den JS-Standard verstehen, dass JIT-Compiler für die Performance entscheidend sind, und steuern die Sprachentwicklung entsprechend
- Sie spielen nicht das Spiel, möglichst schnell immer mehr neue Features hinzuzufügen
1 Kommentare
Hacker-News-Kommentare
Ein weiterer wichtiger Punkt, den der OP übersehen hat: Wenn man eine Alternative Implementation baut, unterscheidet sie sich in der Regel architektonisch von der Referenzimplementierung, und etwas, das in der Referenzimplementierung einfach ist, kann in der eigenen Implementierung sehr schwierig sein.
Nehmen wir zum Beispiel an, eine proprietäre Software für Finanzberichte speichert Dokumente in einem merkwürdigen Binärformat. Beim Bau einer kostenlosen Alternative entscheidet man sich für eine Architektur, die das gesamte Dokument in den Speicher lädt und beim Speichern die ganze Datei neu schreibt; die Originalsoftware dagegen wurde vielleicht in einer Zeit mit wenig RAM entwickelt, liest und schreibt nur den Abschnitt, an dem der Nutzer gerade arbeitet, und kann sogar In-place-Änderungen vornehmen.
Wenn das Original später eine Funktion hinzufügt, um Anhänge in Dokumente einzubetten, funktionieren dank abschnittsweisem Laden auch große Dateien wie Aufzeichnungen von Investor Calls oder gescannte PDFs mit Hunderten Seiten gut. Meine Implementierung hingegen deserialisiert das gesamte Dokument, sodass es problematisch wird, sobald das Dokument größer ist als der RAM des Nutzers; wegen einer Änderung, die im Original ein einzelner Entwickler in einer Woche erledigen könnte, müsste man unter Umständen die gesamte Software neu entwerfen.
Umgekehrt gab es auch Features, die im Original schwerer zu implementieren waren, in der zweiten Implementierung aber trivial wurden. Ein großes Redesign gab es nur einmal, und das betraf die ursprüngliche Implementierung, die Annahmen enthielt, die zu exponentiellem Wachstum führten.
Ein öffentliches Beispiel ist das Ausführen von Windows-Anwendungen unter Linux. Der Linux-Kernel ist eine völlig andere Implementierung als NT und zielt auch nicht auf Kompatibilität ab, aber um Windows-Apps auszuführen, muss nicht der gesamte Kernel neu entworfen werden. Ein paar relativ allgemeine Kernel-Funktionen und eine Kompatibilitätsschicht im Userspace reichen. Wine zu schreiben und zu pflegen kostet viel Aufwand, aber deutlich weniger als die Windows-Implementierung selbst, und es läuft auf einer Plattform, die gar nicht auf Windows-Kompatibilität ausgelegt war. Allerdings muss man, wie im Artikel beschrieben, Windows ständig hinterherlaufen, und es entsteht sogar Code, der sich auf Bugs der Referenzimplementierung verlässt; um also bugkompatibel zu sein, muss man zuerst herausfinden, welche Bugs man implementieren muss.
Deshalb kann beliebiger Code zur Laufzeit andere Dinge verändern. In der Praxis wird das nicht besonders häufig genutzt, aber wenn man versucht, es zu entfernen, gibt es einen Aufschrei. Eine Implementierung, die Python tatsächlich kompiliert, muss sogar den Fall behandeln, dass irgendein Thread plötzlich etwas unter einem anderen Thread verändert.
Das ist eine Möglichkeit, wie ein kleines Unternehmen die Last eines großen Unternehmens vergrößern kann; dasselbe gilt für Unternehmen, die technische Schulden weniger vernachlässigt haben. Es ist auch einer der seltenen Momente, in denen man dem Management technische Schulden klar vor Augen führen kann, weil man sagen kann: „Wir brauchen länger als Acme, um dieses Feature zu implementieren.“
Der Aussage „Versucht nicht, eine Teilmenge von Python zu bauen“ stimme ich zu. Projekte, die als „Python, aber X ist besser“ vermarktet werden, haben es immer schwer, mit der Referenzimplementierung zu konkurrieren, besonders wenn X Geschwindigkeit ist. Wer eine dynamisch typisierte Sprache nutzt, kümmert sich am Ende oft nicht besonders stark um die Ausführungsgeschwindigkeit.
Aber alternative Implementierungen scheitern nicht immer. MicroPython scheint recht erfolgreich zu sein, obwohl es kaum mehr als den Stand von Python 3.4 unterstützt. Es ist darauf ausgelegt, auf Mikrocontrollern zu laufen, und konkurriert daher nicht mit CPython, sondern mit anderen Programmierumgebungen für Mikrocontroller.
Allerdings bekommen die Maintainer von MicroPython vermutlich viele Anfragen nach neueren Python-Features. Ich hatte einmal über eine leichtgewichtige, aufs Einbetten in Anwendungen fokussierte Python-Alternative nachgedacht; auch in diesem Fall sollte sie nicht mit CPython, sondern mit Lua konkurrieren. Die Feature-Anfrage Nummer eins war dann aber: „Unterstützt es NumPy?“
Gleichzeitig scheint auch auf CPython-Seite daran gearbeitet zu werden, es besser im Frontend laufen zu lassen; der zentrale Schmerzpunkt ist die Paketgröße.
Beim Aufbau eines Startups habe ich etwas Ähnliches gelernt. Wenn ich es noch einmal machen würde, hätte ich die Table-Stakes-Features in unserem Bereich aktiv vermieden.
Stattdessen hätten wir nur das Minimum bauen sollen, das Vertrauen schafft, dass unsere Architektur Enterprise-Anforderungen unterstützen kann, und uns voll auf Differenzierungsmerkmale konzentrieren sollen, die Reaktionen wie „Ah, ich sehe, wohin das führen kann“ auslösen — nicht auf Features, die nur ein „Das ist einfach ein Klon von X“ hervorrufen.
Die beschriebene Strategie ist eher: die grundlegenden Pflichtfunktionen implementieren, aber danach nicht tief in die „üblichen“ Erweiterungen gehen. Also mit interessanten Funktionen dafür sorgen, dass man erneut kontaktiert wird, und zugleich genug Basisfunktionen haben, um nicht wegen fehlender Muss-Kriterien auszuscheiden.
Bei allem Wrapper-Code fühlt es sich ähnlich an. Manchmal sagt jemand: „Wir brauchen eine interne Version dieser API.“
Die Gründe sind unterschiedlich, laufen aber oft auf etwas wie „Man kann nicht darauf vertrauen, dass die offizielle API richtig verwendet wird“ hinaus. Das mag sein, aber die interne Version ist weniger standardisiert und schlechter dokumentiert.
Manchmal lautet der Grund: „Wir brauchen zusätzliche Funktionen.“ Dann sollte man aber nicht die ganze API umhüllen, sondern einfach drei Funktionen ergänzen. Mit der Zeit kann 99 % der Codebasis zu einem Polyfill werden.
Der Punkt ist: Wenn man nicht die Defaults verwendet, bereitet man der Person, die später die Codebasis übernimmt, große Schmerzen.
Die Library ziggy-pydust, mit der man Python-Nativmodule in Zig schreibt, sieht zum Beispiel deutlich besser aus als ein gewöhnlicher
Python.h-Import. Trotzdem gibt es auch.ffi, um direkt auf Funktionen zuzugreifen, die zwar inPython.hvorhanden, aber noch nicht implementiert sind.Ohne eine solche Option wirft man so eine Library meistens weg und bevorzugt das Original. Allerdings ist auch das in gewisser Weise ein Wrapper, nämlich ein Nativmodul, und für die Entwicklungsgeschwindigkeit kann es manchmal besser sein, direkt ctypes zu verwenden.
Ein guter Artikel mit vielen starken Lehren, aber ein zentraler Bestandteil fehlt. Das ähnelt jeder konkurrierenden Alternative zu einem Produkt.
Es ist ein bisschen so, als würde man sagen, Amazon sei gescheitert, weil es keine stationären Buchläden hatte, an die die Leute gewöhnt waren; tatsächlich war das aber nicht so.
Der Grund, warum diese JIT-Alternativen gescheitert sind und ständig nur aufgeholt haben, ist in der Praxis, dass die meisten Entwickler der Sprache X JIT nicht für so wichtig halten. Genauer gesagt halten sie Sprachfeatures und Interoperabilität für wichtiger als JIT.
Deshalb gewinnt ein Produkt, das nicht konkurriert, sondern „beitritt“. Denn es kann keine höhere Stabilität oder Interoperabilität bieten.
Ich beschäftige mich seit Langem mit Sprachen und Compilern, und dieser Artikel war sehr interessant. Anders ausgedrückt: Eine Sprache ist viel mehr als nur Kompiliergeschwindigkeit.
Kompiliergeschwindigkeit ist sehr wichtig und gehört sicher zu den zehn wichtigsten Dimensionen. Vor allem beschleunigt eine höhere Kompiliergeschwindigkeit die Feedback-Loops der Entwickler, und das Kernteam kann dadurch auch alle anderen Dimensionen schneller verbessern.
Trotzdem gibt es bei Programmiersprachen noch mehr als 30 weitere sehr wichtige Dimensionen.
Die Schlussfolgerungen dazu, wie solche Projekte erfolgreich sein können, sind gut, aber ein Faktor, warum viele Projekte nicht abheben, wird noch zu wenig erwähnt: Die Kompatibilität alternativer Implementierungen ist in Wirklichkeit oft geringer als behauptet, selbst bei alten Sprachfeatures.
Ruby- und Python-Apps haben zum Beispiel sehr häufig irgendwo in ihren Abhängigkeiten native C-Erweiterungen, und meines Wissens haben die großen alternativen Implementierungen diese nie unterstützt. Es gab Versuche, aber aus offensichtlichen technischen Gründen funktionierte das nicht gut, und auch die Alternative, von Libraries mehrere Implementierungen zu erwarten, hatte historisch keinen einfachen Verlauf.
Kombiniert man das mit der Tatsache, dass diese Sprachen oft für CRUD-Websites genutzt werden, bei denen I/O ein größerer Performance-Faktor ist als die CPU, sinkt die Attraktivität einer schnelleren Alternative deutlich.
Wirklich ein hervorragender Artikel. Die Soziologie der Technologie ist sehr interessant.
Maintainer von Sprachimplementierungen wollen maximale Flexibilität, wenn sie neue Features entwerfen und ausliefern, die den Nutzern zugutekommen. Sie möchten nicht dadurch ausgebremst werden, dass sie vor der Veröffentlichung eines Features erst den Konsens mehrerer Implementierungen einholen müssen. Als Beispiele kann man sehen, wie gletscherhaft langsam sich JavaScript über Jahre weiterentwickelt hat und wie schnell TypeScript im Vergleich dazu evolviert.
Gleichzeitig können alternative Implementierungen ein Signal für ein robustes Sprachökosystem sein und haben daher auch Vorteile. Wenn eine alternative Implementierung wirklich gut ist, kann sie dem Ökosystem echten Wert hinzufügen, selbst wenn sie nur für Nutzer mit bestimmten Nischenanforderungen gut ist.
Als Sprachdesigner oder Maintainer wäre man alternativen Implementierungen deshalb wohl nicht aktiv feindlich gesinnt, aber es gibt Nachteile. Im Allgemeinen kommt Nutzerfeedback eher in die Richtung, neue Features auszuliefern und die Sprache weiterzuentwickeln. Es wird nicht viele Bitten geben, langsamer zu machen, damit PyPy, IronRuby, LuaJIT und andere mithalten können.
Wenn Sprachkonsumenten entscheiden, auf welcher Implementierung sie aufbauen, haben Sicherheit und Stabilität meist höchste Priorität. Niemand möchte, dass eine Codebasis mit einer Million Zeilen von subtilen Verhaltensmerkmalen einer alternativen Implementierung abhängt, die einst von einem sehr klugen Doktoranden gebaut wurde, der inzwischen zu einem anderen Projekt weitergezogen ist. Deshalb strömen Nutzer zur meistverwendeten Implementierung, und diese Tatsache zieht wiederum weitere Nutzer an; so entsteht eine starke positive Feedback-Schleife.
Infolgedessen konvergieren die meisten Sprachen auf eine Referenzimplementierung, sofern es keine starke Kraft gibt, die in die Gegenrichtung drückt. Man kann argumentieren, dass das eine gute Sache ist, weil fast der gesamte Engineering-Aufwand für Sprachimplementierungen allen Nutzern zugutekommt, statt auf mehrere Implementierungen verteilt zu werden. Der Nachteil ist natürlich, dass eine Implementierung in einem lokalen Optimum stecken bleiben kann.
LuaJIT wurde erwähnt, ist aber auch ein Beispiel dafür, dass es nicht immer so läuft wie die Schlussfolgerung des Artikels. Viele Menschen und Projekte haben sich bewusst für LuaJIT statt Lua entschieden.
luajittexist nebenluatexebenfalls leicht verfügbar.Vielleicht ist das ein weniger populärer Gedanke, aber Menschen sollten – mich eingeschlossen – gelegentlich ihren Stolz überprüfen. Es kann einfacher sein, ein paralleles Projekt zu bauen, das „meins“ ist, als zu einem bestehenden Open-Source-Projekt beizutragen, aber man sollte sich fragen, für wen man das tut.
Für die Maintainer des Projekts, für das Projekt selbst, für die Nutzer oder für den eigenen Stolz? Wenn einen der letzte Punkt wütend macht, ist er vermutlich relevant.
Einer bestehenden Sprache JIT hinzuzufügen, ist eine große Aufgabe, daher werden auch die Hürden hoch sein, damit die Referenzimplementierung es akzeptiert. Trotzdem denke ich, dass das das Ziel sein sollte. Ein Fork oder ein kompletter Neubau gibt einem ebenfalls die Freiheit, große Dinge zu tun, sollte aber oft als vorübergehend betrachtet werden.
Wenn das Ziel ist zu zeigen, was ich kann, kommt man wahrscheinlich nicht weit. Wenn das Ziel ist, etwas Besseres zu schaffen, lernt man, innerhalb der Einschränkungen anderer zu arbeiten.
Ich denke, dieser Artikel zeigt, wie Python, Lua und Ruby mit diesem Ansatz viele Menschen enttäuscht haben. Das Ergebnis ist, dass Tausende Entwickler und Millionen Nutzer langsamere Entwicklung und langsamere Software hinnehmen müssen. Nicht, weil es unmöglich wäre, sondern weil es administrativ keinen Anreiz dafür gibt.