更新(2020年3月2日)
ここでの私の例のコーディングは、V8 JavaScriptエンジンの既知のパフォーマンスの崖から落ちるように正しい方法で構成されていることがわかります...
詳細については、bugs.chromium.orgに関する議論を参照してください。このバグは現在取り組んでおり、近い将来に修正される予定です。
アップデート(2020年1月9日)
以下で説明するように動作するコーディングを単一ページのWebアプリに分離しようとしましたが、そうすると、動作が消えました(??)。ただし、以下に説明する動作は、完全なアプリケーションのコンテキストではまだ存在します。
とはいえ、私はフラクタル計算コーディングを最適化しており、この問題はライブバージョンでは問題ではなくなりました。誰かが興味を持っているなら、この問題を明らかにするJavaScriptモジュールはまだここで利用可能です
概観
ブラウザベースのJavaScriptとWebアセンブリのパフォーマンスを比較するための小さなWebベースのアプリを完成させました。このアプリはマンデルブロ集合の画像を計算し、その画像の上にマウスポインターを移動すると、対応するジュリア集合が動的に計算され、計算時間が表示されます。
JavaScript(「j」を押す)またはWebAssembly(「w」を押す)の使用を切り替えて、計算を実行し、ランタイムを比較できます。
ここをクリックして、動作するアプリを確認してください
ただし、このコードを作成する際に、予期しない奇妙なJavaScriptパフォーマンス動作がいくつか見つかりました...
問題の概要
この問題は、ChromeとBraveで使用されているV8 JavaScriptエンジンに固有のようです。この問題は、SpiderMonkey(Firefox)またはJavaScriptCore(Safari)を使用するブラウザでは発生しません。チャクラエンジンを使用しているブラウザでこれをテストできませんでした
このWebアプリのすべてのJavaScriptコードはES6モジュールとして記述されています
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倍向上することがわかりました。juliaYStepFn
for
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倍速く実行されます。
誰かがこの動作を説明できますか?
ありがとう