Update (2. März 2020)
Es stellt sich heraus, dass die Codierung in meinem Beispiel hier genau so strukturiert war, dass sie von einer bekannten Leistungsklippe in der V8-JavaScript-Engine abfällt ...
Weitere Informationen finden Sie in der Diskussion auf bugs.chromium.org . Dieser Fehler wird derzeit bearbeitet und sollte in naher Zukunft behoben werden.
Update (9. Januar 2020)
Ich habe versucht, die Codierung, die sich auf die unten beschriebene Weise verhält, in einer einseitigen Web-App zu isolieren, aber dabei ist das Verhalten verschwunden (??). Das unten beschriebene Verhalten besteht jedoch weiterhin im Kontext der vollständigen Anwendung.
Trotzdem habe ich seitdem die Codierung der Fraktalberechnung optimiert und dieses Problem ist in der Live-Version kein Problem mehr. Sollte jemand interessiert sein, die JavaScript - Modul , dass Manifeste dieses Problem ist noch verfügbar hier
Überblick
Ich habe gerade eine kleine webbasierte App fertiggestellt, um die Leistung von browserbasiertem JavaScript mit Web Assembly zu vergleichen. Diese App berechnet ein Mandelbrot-Set-Bild. Wenn Sie den Mauszeiger über dieses Bild bewegen, wird das entsprechende Julia-Set dynamisch berechnet und die Berechnungszeit angezeigt.
Sie können zwischen JavaScript (drücken Sie 'j') oder WebAssembly (drücken Sie 'w') wechseln, um die Berechnung durchzuführen und die Laufzeiten zu vergleichen.
Klicken Sie hier , um die funktionierende App anzuzeigen
Beim Schreiben dieses Codes entdeckte ich jedoch ein unerwartet seltsames JavaScript-Leistungsverhalten ...
Problemübersicht
Dieses Problem scheint spezifisch für die in Chrome und Brave verwendete V8-JavaScript-Engine zu sein. Dieses Problem tritt nicht in Browsern auf, die SpiderMonkey (Firefox) oder JavaScriptCore (Safari) verwenden. Ich konnte dies nicht in einem Browser mit der Chakra-Engine testen
Der gesamte JavaScript-Code für diese Web-App wurde als ES6-Module geschrieben
Ich habe versucht, alle Funktionen mit der traditionellen
function
Syntax anstelle der neuen ES6-Pfeilsyntax neu zu schreiben . Dies macht leider keinen nennenswerten Unterschied
Das Leistungsproblem scheint sich auf den Bereich zu beziehen, in dem eine JavaScript-Funktion erstellt wird. In dieser App rufe ich zwei Teilfunktionen auf, von denen jede mir eine andere Funktion zurückgibt. Ich übergebe diese generierten Funktionen dann als Argumente an eine andere Funktion, die in einer verschachtelten for
Schleife aufgerufen wird.
In Bezug auf die Funktion, in der es ausgeführt wird, scheint es, dass eine for
Schleife etwas erzeugt, das ihrem eigenen Bereich ähnelt (nicht sicher, ob es sich jedoch um einen vollständigen Bereich handelt). Das Übergeben generierter Funktionen über diese Bereichsgrenze (?) Ist dann teuer.
Grundlegende Codierungsstruktur
Jede Teilfunktion empfängt den X- oder Y-Wert der Position des Mauszeigers über dem Mandelbrot-Set-Bild und gibt die Funktion zurück, die bei der Berechnung des entsprechenden Julia-Sets iteriert werden soll:
const makeJuliaXStepFn = mandelXCoord => (x, y) => mandelXCoord + diffOfSquares(x, y)
const makeJuliaYStepFn = mandelYCoord => (x, y) => mandelYCoord + (2 * x * y)
Diese Funktionen werden innerhalb der folgenden Logik aufgerufen:
- Der Benutzer bewegt den Mauszeiger über das Bild des Mandelbrot-Sets, das das
mousemove
Ereignis auslöst Die aktuelle Position des Mauszeigers wird in den Koordinatenraum des Mandelbrot-Satzes übersetzt, und die (X, Y) -Koordinaten werden an die Funktion übergeben
juliaCalcJS
, um den entsprechenden Julia-Satz zu berechnen.Beim Erstellen eines bestimmten Julia-Sets werden die beiden oben genannten Teilfunktionen aufgerufen, um die Funktionen zu generieren, die beim Erstellen des Julia-Sets iteriert werden sollen
Eine verschachtelte
for
Schleife ruft dann die FunktionjuliaIter
auf, um die Farbe jedes Pixels in der Julia-Menge zu berechnen. Die vollständige Codierung ist zu sehen, hier , aber die wesentliche Logik ist wie folgt:const juliaCalcJS = (cvs, juliaSpace) => { // Snip - initialise canvas and create a new image array // Generate functions for calculating the current Julia Set let juliaXStepFn = makeJuliaXStepFn(juliaSpace.mandelXCoord) let juliaYStepFn = makeJuliaYStepFn(juliaSpace.mandelYCoord) // For each pixel in the canvas... for (let iy = 0; iy < cvs.height; ++iy) { for (let ix = 0; ix < cvs.width; ++ix) { // Translate pixel values to coordinate space of Julia Set let x_coord = juliaSpace.xMin + (juliaSpace.xMax - juliaSpace.xMin) * ix / (cvs.width - 1) let y_coord = juliaSpace.yMin + (juliaSpace.yMax - juliaSpace.yMin) * iy / (cvs.height - 1) // Calculate colour of the current pixel let thisColour = juliaIter(x_coord, y_coord, juliaXStepFn, juliaYStepFn) // Snip - Write pixel value to image array } } // Snip - write image array to canvas }
Wie Sie sehen können, werden die Funktionen, die beim Aufrufen
makeJuliaXStepFn
undmakeJuliaYStepFn
außerhalb derfor
Schleife zurückgegeben werden, übergeben,juliaIter
die dann die ganze harte Arbeit der Berechnung der Farbe des aktuellen Pixels erledigen
Als ich mir diese Codestruktur ansah, dachte ich zuerst: "So gut, alles funktioniert gut; hier ist also nichts falsch."
Außer es gab. Die Leistung war viel langsamer als erwartet ...
Unerwartete Lösung
Es folgte viel Kopfkratzen und Herumspielen ...
Nach einer gewissen Zeit stellte ich fest, daß , wenn ich die Schaffung von Funktionen bewegen juliaXStepFn
und juliaYStepFn
innerhalb entweder der äußeren oder inneren for
Schleifen, dann um einen Faktor zwischen 2 und 3 wird die Leistung verbessert ...
WHAAAAAAT!?
Der Code sieht jetzt so aus
const juliaCalcJS =
(cvs, juliaSpace) => {
// Snip - initialise canvas and create a new image array
// For each pixel in the canvas...
for (let iy = 0; iy < cvs.height; ++iy) {
// Generate functions for calculating the current Julia Set
let juliaXStepFn = makeJuliaXStepFn(juliaSpace.mandelXCoord)
let juliaYStepFn = makeJuliaYStepFn(juliaSpace.mandelYCoord)
for (let ix = 0; ix < cvs.width; ++ix) {
// Translate pixel values to coordinate space of Julia Set
let x_coord = juliaSpace.xMin + (juliaSpace.xMax - juliaSpace.xMin) * ix / (cvs.width - 1)
let y_coord = juliaSpace.yMin + (juliaSpace.yMax - juliaSpace.yMin) * iy / (cvs.height - 1)
// Calculate colour of the current pixel
let thisColour = juliaIter(x_coord, y_coord, juliaXStepFn, juliaYStepFn)
// Snip - Write pixel value to image array
}
}
// Snip - write image array to canvas
}
Ich hätte erwartet, dass diese scheinbar unbedeutende Änderung etwas weniger effizient ist, da jedes Mal, wenn wir die for
Schleife durchlaufen, zwei Funktionen neu erstellt werden, die nicht geändert werden müssen . Durch Verschieben der Funktionsdeklarationen innerhalb der for
Schleife wird dieser Code jedoch zwei- bis dreimal schneller ausgeführt!
Kann jemand dieses Verhalten erklären?
Vielen Dank