paint-brush
Warum ist JavaScript-Benchmarking ein Chaos?von@asyncbanana
Neue Geschichte

Warum ist JavaScript-Benchmarking ein Chaos?

von AsyncBanana7m2025/01/04
Read on Terminal Reader

Zu lang; Lesen

JavaScript-Benchmarking ist nach wie vor notwendig, insbesondere da JavaScript in leistungssensitiveren Anwendungen verwendet wird. JavaScript-Engines haben ihr Bestes getan, um Timing-Angriffe abzuschwächen, und sind gleichzeitig dazu übergegangen, auf [Do Not Track] umzusteigen. JavaScript-Engines verfälschen Timing-Messungen absichtlich.
featured image - Warum ist JavaScript-Benchmarking ein Chaos?
AsyncBanana HackerNoon profile picture
0-item
1-item

Ich hasse es, Code zu vergleichen, genau wie jeder Mensch (was die meisten Leser dieses Artikels an dieser Stelle wahrscheinlich nicht tun ¯\ (ツ) /¯). Es macht viel mehr Spaß, so zu tun, als ob das Zwischenspeichern eines Werts die Leistung um 1000 % erhöht hätte, als zu testen, was es bewirkt hat. Leider ist das Vergleichen von JavaScript immer noch notwendig, insbesondere da JavaScript ( obwohl es nicht verwendet werden sollte? ) in leistungsempfindlicheren Anwendungen verwendet wird. Leider macht JavaScript das Vergleichen aufgrund vieler seiner grundlegenden Architekturentscheidungen nicht einfacher.

Was ist falsch an JavaScript?

Der JIT-Compiler verringert die Genauigkeit(?)

Wer mit der Magie moderner Skriptsprachen wie JavaScript nicht vertraut ist, sollte wissen, dass deren Architektur ziemlich komplex sein kann. Anstatt Code einfach durch einen Interpreter laufen zu lassen, der sofort Anweisungen ausspuckt, verwenden die meisten JavaScript-Engines eine Architektur, die eher einer kompilierten Sprache wie C ähnelt – sie integrieren mehrere Ebenen von „Compilern“ .


Jeder dieser Compiler bietet einen anderen Kompromiss zwischen Kompilierungszeit und Laufzeitleistung, sodass der Benutzer keine Rechenzeit mit der Optimierung von Code verbringen muss, der selten ausgeführt wird, und stattdessen die Leistungsvorteile des fortgeschritteneren Compilers für den Code nutzen kann, der am häufigsten ausgeführt wird (die „Hot Paths“). Bei der Verwendung von optimierenden Compilern treten auch einige andere Komplikationen auf, die mit ausgefallenen Programmierbegriffen wie „ Funktionsmonomorphismus “ verbunden sind, aber ich werde Sie verschonen und hier nicht darüber sprechen.


Also … warum ist das für das Benchmarking wichtig? Nun, wie Sie vielleicht schon vermutet haben, da beim Benchmarking die Leistung von Code gemessen wird, kann der JIT-Compiler einen ziemlich großen Einfluss haben. Bei kleineren Codeteilen kann beim Benchmarking nach vollständiger Optimierung oft eine Leistungssteigerung von über 10x erzielt werden, was zu vielen Fehlern in den Ergebnissen führt.


Beispielsweise in Ihrem grundlegendsten Benchmarking-Setup (verwenden Sie aus mehreren Gründen nichts wie das Folgende):

 for (int i = 0; i<1000; i++) { console.time() // do some expensive work console.timeEnd() }

(Keine Sorge, wir werden auch über console.time sprechen)


Ein Großteil Ihres Codes wird nach einigen Versuchen zwischengespeichert, was die Zeit pro Vorgang erheblich verkürzt. Benchmarkprogramme tun oft ihr Bestes, um dieses Zwischenspeichern/Optimieren zu vermeiden, da es auch dazu führen kann, dass später im Benchmarkprozess getestete Programme relativ schneller erscheinen. Sie müssen sich jedoch letztendlich fragen, ob Benchmarks ohne Optimierungen der Leistung in der realen Welt entsprechen.


Sicher, in bestimmten Fällen, wie bei selten aufgerufenen Webseiten, ist eine Optimierung unwahrscheinlich, aber in Umgebungen wie Servern, wo die Leistung am wichtigsten ist, sollte man mit einer Optimierung rechnen. Wenn Sie einen Code als Middleware für Tausende von Anfragen pro Sekunde ausführen, können Sie besser hoffen, dass V8 ihn optimiert.


Im Grunde genommen gibt es sogar innerhalb einer Engine zwei bis vier verschiedene Möglichkeiten, Ihren Code mit unterschiedlichen Leistungsstufen auszuführen. Außerdem ist es in bestimmten Fällen unglaublich schwierig, sicherzustellen, dass bestimmte Optimierungsstufen aktiviert sind. Viel Spaß :).

Motoren tun ihr Bestes, um Sie daran zu hindern, das richtige Timing zu erreichen

Kennen Sie Fingerprinting? Die Technik, die es ermöglichte, Do Not Track zur Unterstützung des Trackings einzusetzen ? Ja, JavaScript-Engines haben ihr Bestes getan, um dies zu verhindern. Diese Bemühungen sowie ein Schritt zur Verhinderung von Timing-Angriffen führten dazu, dass JavaScript-Engines das Timing absichtlich ungenau machten, sodass Hacker keine genauen Messungen der aktuellen Computerleistung oder des Aufwands einer bestimmten Operation erhalten können.


Dies bedeutet leider, dass bei Benchmarks ohne entsprechende Optimierungen das gleiche Problem auftritt.


Das Beispiel im vorherigen Abschnitt ist ungenau, da es nur in Millisekunden misst. Ersetzen Sie es jetzt durch performance.now() . Großartig.


Jetzt haben wir Zeitstempel in Mikrosekunden!

 // Bad console.time(); // work console.timeEnd(); // Better? const t = performance.now(); // work console.log(performance.now() - t);

Außer… sie sind alle in Schritten von 100 μs. Fügen wir nun einige Header hinzu , um das Risiko von Timing-Angriffen zu verringern. Ups, wir können immer noch nur in Schritten von 5 μs. 5 μs ist wahrscheinlich für viele Anwendungsfälle präzise genug, aber für alles, was mehr Granularität erfordert, müssen Sie sich woanders umsehen. Soweit ich weiß, erlaubt kein Browser granularere Timer. Node.js tut das, aber das hat natürlich seine eigenen Probleme.


Selbst wenn Sie sich dazu entschließen, Ihren Code über den Browser laufen zu lassen und den Compiler seine Arbeit machen zu lassen, werden Sie natürlich trotzdem mehr Kopfschmerzen haben, wenn Sie genaues Timing wünschen. Ach ja, und nicht alle Browser sind gleich.

Jede Umgebung ist anders

Ich liebe Bun , weil es serverseitiges JavaScript vorangebracht hat, aber verdammt, es macht das Benchmarking von JavaScript für Server viel schwieriger. Vor ein paar Jahren waren die einzigen serverseitigen JavaScript-Umgebungen, die die Leute interessierten, Node.js und Deno , die beide die V8-JavaScript-Engine verwendeten (dieselbe wie in Chrome). Bun verwendet stattdessen JavaScriptCore, die Engine in Safari, die völlig andere Leistungsmerkmale aufweist.


Dieses Problem mehrerer JavaScript-Umgebungen mit ihren eigenen Leistungsmerkmalen ist bei serverseitigem JavaScript relativ neu, plagt Clients jedoch schon seit langem. Die drei häufig verwendeten JavaScript-Engines V8, JSC und SpiderMonkey für Chrome, Safari und Firefox können bei einem gleichwertigen Codestück alle deutlich schneller oder langsamer arbeiten.


Ein Beispiel für diese Unterschiede ist die Tail Call Optimization (TCO). TCO optimiert Funktionen, die am Ende ihres Körpers rekursiv sind, wie folgt:

 function factorial(i, num = 1) { if (i == 1) return num; num *= i; i--; return factorial(i, num); }


Versuchen Sie factorial(100000) in Bun zu benchmarken. Versuchen Sie nun dasselbe in Node.js oder Deno. Sie sollten eine Fehlermeldung ähnlich dieser erhalten:

 function factorial(i, num = 1) { ^ RangeError: Maximum call stack size exceeded


In V8 (und damit auch in Node.js und Deno) erstellt die Engine jedes Mal, wenn factorial() sich am Ende selbst aufruft, einen völlig neuen Funktionskontext für die Ausführung der verschachtelten Funktion, der letztendlich durch den Aufrufstapel begrenzt wird. Aber warum passiert das nicht in Bun? JavaScriptCore, das Bun verwendet, implementiert TCO, das diese Art von Funktionen optimiert, indem es sie in eine For-Schleife wie diese umwandelt:

 function factorial(i, num = 1) { while (i != 1) { num *= i; i--; } return i; }

Das obige Design vermeidet nicht nur Aufrufstapelbeschränkungen, sondern ist auch viel schneller, weil es keine neuen Funktionskontexte erfordert. Das bedeutet, dass Funktionen wie die oben genannten bei verschiedenen Engines sehr unterschiedliche Benchmarks aufweisen.


Im Wesentlichen bedeuten diese Unterschiede nur, dass Sie alle Engines vergleichen sollten, die Ihren Code ausführen sollen, um sicherzustellen, dass Code, der in einer Engine schnell ist, in einer anderen nicht langsam ist. Wenn Sie eine Bibliothek entwickeln, die voraussichtlich auf vielen Plattformen verwendet wird, sollten Sie außerdem darauf achten, auch etwas exotischere Engines wie Hermes einzubinden, denn diese haben ganz andere Leistungsmerkmale.

Lobende Erwähnungen

  • Der Garbage Collector und seine Tendenz, alles willkürlich anzuhalten.


  • Die Fähigkeit des JIT-Compilers, Ihren gesamten Code zu löschen, weil er „nicht notwendig ist“.


  • Furchtbar breite Flammendiagramme in den meisten JavaScript-Entwicklertools.


  • Ich denke, Sie verstehen, was ich meine.

Also..., was ist die Lösung?

Ich wünschte, ich könnte auf ein NPM-Paket verweisen, das alle diese Probleme löst, aber es gibt wirklich keins.


Auf dem Server haben Sie es etwas einfacher. Sie können d8 verwenden, um die Optimierungsstufen manuell zu steuern, den Garbage Collector zu steuern und genaue Zeitangaben zu erhalten. Natürlich benötigen Sie etwas Bash-Fu, um hierfür eine gut konzipierte Benchmark-Pipeline einzurichten, da d8 leider nicht gut (oder überhaupt nicht) in Node.js integriert ist.


Sie können in Node.js auch bestimmte Flags aktivieren, um ähnliche Ergebnisse zu erzielen, aber dann fehlen Ihnen Funktionen wie das Aktivieren bestimmter Optimierungsebenen.

 v8 --sparkplug --always-sparkplug --no-opt [file]

Ein Beispiel für D8 mit einer bestimmten aktivierten Kompilierungsebene (Sparkplug). D8 enthält standardmäßig mehr Kontrolle über GC und allgemein mehr Debuginformationen.


Sie können einige ähnliche Funktionen auf JavaScriptCore erhalten??? Ehrlich gesagt habe ich die CLI von JavaScriptCore nicht viel verwendet und sie ist stark unterdokumentiert. Sie können bestimmte Ebenen mithilfe ihrer Befehlszeilenflags aktivieren, aber ich bin nicht sicher, wie viele Debuginformationen Sie abrufen können. Bun enthält auch einige hilfreiche Benchmarking-Dienstprogramme , aber sie sind ähnlich wie Node.js eingeschränkt.


Leider erfordert all dies die Basis-Engine/Testversion der Engine, die ziemlich schwer zu bekommen sein kann. Ich habe festgestellt, dass die einfachste Möglichkeit zum Verwalten von Engines esvu in Kombination mit eshost-cli ist, da sie zusammen das Verwalten von Engines und das Ausführen von Code auf ihnen erheblich vereinfachen. Natürlich ist immer noch viel manuelle Arbeit erforderlich, da diese Tools nur das Ausführen von Code auf verschiedenen Engines verwalten – Sie müssen den Benchmarking-Code immer noch selbst schreiben.


Wenn Sie nur versuchen, eine Engine mit Standardoptionen so genau wie möglich auf dem Server zu benchmarken, gibt es handelsübliche Node.js-Tools wie Mitata , die dabei helfen, die Zeitgenauigkeit und GC-bezogene Fehler zu verbessern. Viele dieser Tools, wie Mitata, können auch für viele Engines verwendet werden; natürlich müssen Sie trotzdem eine Pipeline wie die oben beschriebene einrichten.


Im Browser ist alles viel komplizierter. Ich kenne keine Lösungen für präziseres Timing und die Kontrolle über die Engine ist viel eingeschränkter. Die meisten Informationen zur Laufzeitleistung von JavaScript im Browser erhalten Sie in den Chrome-Devtools , die grundlegende Dienstprogramme für Flame-Graph- und CPU-Verlangsamungssimulationen bieten.

Abschluss

Viele der Designentscheidungen, die JavaScript (relativ) leistungsfähig und portabel gemacht haben, machen Benchmarking deutlich schwieriger als bei anderen Sprachen. Es gibt viel mehr Ziele zum Benchmarking und Sie haben viel weniger Kontrolle über jedes Ziel.


Hoffentlich wird es eines Tages eine Lösung geben, die viele dieser Probleme löst. Vielleicht werde ich irgendwann ein Tool entwickeln, das Benchmarking zwischen verschiedenen Engines und Kompilierungsebenen vereinfacht, aber im Moment ist es noch ziemlich aufwändig, eine Pipeline zu erstellen, die all diese Probleme löst. Natürlich ist es wichtig, sich daran zu erinnern, dass diese Probleme nicht für alle gelten – wenn Ihr Code nur in einer Umgebung ausgeführt wird, verschwenden Sie Ihre Zeit nicht mit Benchmarking in anderen Umgebungen.


Wie auch immer Sie Benchmarking durchführen, ich hoffe, dieser Artikel hat Ihnen einige der Probleme aufgezeigt, die beim JavaScript-Benchmarking auftreten. Lassen Sie mich wissen, ob ein Tutorial zur Implementierung einiger der oben beschriebenen Dinge hilfreich wäre.