Kann jemand dieses unerwartete Leistungsverhalten von V8-JavaScript erklären?


8

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

  1. 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

  2. Der gesamte JavaScript-Code für diese Web-App wurde als ES6-Module geschrieben

  3. Ich habe versucht, alle Funktionen mit der traditionellen functionSyntax 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 forSchleife aufgerufen wird.

In Bezug auf die Funktion, in der es ausgeführt wird, scheint es, dass eine forSchleife 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 mousemoveEreignis 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 forSchleife ruft dann die Funktion juliaIterauf, 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 makeJuliaXStepFnund makeJuliaYStepFnaußerhalb der forSchleife zurückgegeben werden, übergeben, juliaIterdie 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 juliaXStepFnund juliaYStepFninnerhalb entweder der äußeren oder inneren forSchleifen, 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 forSchleife durchlaufen, zwei Funktionen neu erstellt werden, die nicht geändert werden müssen . Durch Verschieben der Funktionsdeklarationen innerhalb der forSchleife wird dieser Code jedoch zwei- bis dreimal schneller ausgeführt!

Kann jemand dieses Verhalten erklären?

Vielen Dank


Fragen (kann helfen, ich weiß nicht): Welche Browser verwenden Sie? Ist der Leistungsgewinn nur in js oder auch in der Webassembly spürbar?
Calculuswhiz

1
Vielen Dank an @Calculuswhiz, dies scheint ein Chrome / Brave-spezifisches Problem zu sein. Safari und Firefox scheinen nicht betroffen zu sein. Ich werde den Beitrag entsprechend aktualisieren
Chris W

1
Dies ist eine sehr detaillierte Zusammenfassung ... Gibt es einen Grund, warum Sie ein V8-Ticket auf einer allgemeinen Programmier-Q & A-Website und nicht auf dem V8-Issue-Tracker eingereicht haben ?
Mike 'Pomax' Kamermans

2
Er hat ein Problem auf dem V8-Tracker gepostet. Es ist das erste dort
Jeremy Gottfried

4
Ich vermute, dass alles in der Iteration das Abhängigkeitsdiagramm für den Optimierer vereinfacht, der dann besseren Code erzeugen kann. Ein v8-Profiler könnte mehr Licht ins Dunkel bringen.
Rustyx

Antworten:


Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.