誰もがこの予期しないV8 JavaScriptパフォーマンス動作を説明できますか?


8

更新(2020年3月2日)

ここでの私の例のコーディングは、V8 JavaScriptエンジンの既知のパフォーマンスの崖から落ちるように正しい方法で構成されていることがわかります...

詳細については、bugs.chromium.orgに関する議論を参照してください。このバグは現在取り組んでおり、近い将来に修正される予定です。

アップデート(2020年1月9日)

以下で説明するように動作するコーディングを単一ページのWebアプリに分離しようとしましたが、そうすると、動作が消えました(??)。ただし、以下に説明する動作は、完全なアプリケーションのコンテキストではまだ存在します。

とはいえ、私はフラクタル計算コーディングを最適化しており、この問題はライブバージョンでは問題ではなくなりました。誰かが興味を持っているなら、この問題を明らかにするJavaScriptモジュールはまだここで利用可能です

概観

ブラウザベースのJavaScriptとWebアセンブリのパフォーマンスを比較するための小さなWebベースのアプリを完成させました。このアプリはマンデルブロ集合の画像を計算し、その画像の上にマウスポインターを移動すると、対応するジュリア集合が動的に計算され、計算時間が表示されます。

JavaScript(「j」を押す)またはWebAssembly(「w」を押す)の使用を切り替えて、計算を実行し、ランタイムを比較できます。

ここをクリックて、動作するアプリを確認してください

ただし、このコードを作成する際に、予期しない奇妙なJavaScriptパフォーマンス動作がいくつか見つかりました...

問題の概要

  1. この問題は、ChromeとBraveで使用されているV8 JavaScriptエンジンに固有のようです。この問題は、SpiderMonkey(Firefox)またはJavaScriptCore(Safari)を使用するブラウザでは発生しません。チャクラエンジンを使用しているブラウザでこれをテストできませんでした

  2. このWebアプリのすべてのJavaScriptコードはES6モジュールとして記述されています

  3. function新しいES6矢印構文ではなく、従来の構文を使用してすべての関数を書き直してみました。残念ながら、これはそれほど大きな違いはありません

パフォーマンスの問題は、JavaScript関数が作成されるスコープに関連しているようです。このアプリでは、2つの部分関数を呼び出し、それぞれが別の関数を返します。次に、これらの生成された関数を、ネストされたforループ内で呼び出される別の関数に引数として渡します。

forループが実行される関数と比較すると、ループは独自のスコープに似たものを作成しているように見えます(ただし、本格的なスコープはわかりません)。次に、このスコープ(?)の境界を越えて生成された関数を渡すと、負荷が高くなります。

基本的なコーディング構造

各部分関数は、マンデルブロ集合イメージ上のマウスポインターの位置のXまたはY値を受け取り、対応するジュリア集合を計算するときに反復される関数を返します。

const makeJuliaXStepFn = mandelXCoord => (x, y) => mandelXCoord + diffOfSquares(x, y)
const makeJuliaYStepFn = mandelYCoord => (x, y) => mandelYCoord + (2 * x * y)

これらの関数は、次のロジック内で呼び出されます。

  • ユーザーは、mousemoveイベントをトリガーするマンデルブロ集合の画像の上にマウスポインターを移動します
  • マウスポインターの現在の位置がマンデルブロ集合の座標空間に変換され、(X、Y)座標がjuliaCalcJS対応するジュリア集合を計算する関数に渡されます。

  • 特定のジュリアセットを作成すると、上記の2つの部分関数が呼び出され、ジュリアセットを作成するときに反復される関数が生成されます。

  • for次に、ネストされたループが関数juliaIterを呼び出して、ジュリアセットのすべてのピクセルの色を計算します。完全なコーディングはこちらで確認できますが、基本的なロジックは次のとおりです。

    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
      }
    
  • ご覧のとおり、呼び出しによって返された関数makeJuliaXStepFnとループのmakeJuliaYStepFn外側にfor渡された関数juliaIterは、現在のピクセルの色を計算するためのハードワークをすべて実行します。

このコード構造を見ると、最初は「これで問題なく、すべてうまく機能するので、ここでは何も問題はない」と思いました。

ありましたが。パフォーマンスは予想よりもはるかに遅かった...

予期しない解決策

頭を掻き回して、いじくり回した後...

しばらくして、関数の作成を外部ループまたは内部ループのいずれかに移動するjuliaXStepFnと、パフォーマンスが2〜3倍向上することがわかりました。juliaYStepFnfor

WHAAAAAAT !?

したがって、コードは次のようになります

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
  }

forループを繰り返すたびに変更する必要のない関数のペアが再作成されるため、この一見重要ではない変更の効率がいくらか低下すると予想していました。ただし、関数宣言をforループ内に移動すると、このコードは2〜3倍速く実行されます。

誰かがこの動作を説明できますか?

ありがとう


質問(助けになるかもしれませんが、わかりません):どのブラウザーを使用していますか?パフォーマンスの向上は、jsまたはwebassemblyでも顕著ですか?
Calculuswhiz

1
@Calculuswhizに感謝します。これはChrome / Brave固有の問題のようです。SafariとFirefoxは影響を受けていないようです。私はそれに応じて投稿を更新します
Chris W

1
これは非常に詳細な要約です。V8の問題トラッカーなく、一般的なプログラミングQ&A Webサイトで基本的にV8チケットを提出した理由はありますか?
Mike 'Pomax' Kamermans

2
彼はV8トラッカーに問題を投稿しました。それが最初のものです
ジェレミー・ゴットフリード

4
私の推測では、反復内にすべてがあると、オプティマイザの依存関係グラフが簡略化され、最適化コードを生成できるようになります。V8プロファイラは、何が起こっているかの上にいくつかのより多くの光を当てることがあります。
rustyx

回答:


弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.