最適化せずに高性能のJavascriptコードを書く


10

大きな数値配列を操作するJavascriptでパフォーマンスに敏感なコードを書くとき(線形代数パッケージ、整数または浮動小数点数を操作することを考えてください)、JITができる限り役立つことを常に望んでいます。これはおおよその意味です。

  1. 整数計算と浮動小数点計算のどちらを実行するかに応じて、常に配列をパックSMI(短整数)またはパックDoubleにする必要があります。
  2. 私たちは常に同じタイプのものを関数に渡したいので、それらが「メガモーフィック」とラベル付けされて最適化されないようにします。たとえば、常にvec.add(x, y)両方xを呼び出しyてパックされたSMI配列、または両方をパックされたDouble配列で呼び出す必要があります。
  3. 関数をできるだけインライン化したい。

これらのケースから外れると、突然の大幅なパフォーマンス低下が発生します。これは、さまざまな無害な理由で発生する可能性があります。

  1. のような一見無害な操作を介して、パックされたSMI配列をパックされたDouble配列に変換できmyArray.map(x => -x)ます。パックされたDouble配列は依然として非常に高速であるため、これは実際には「最良の」悪いケースです。
  2. たとえば、(予期せず)返された、nullまたはを返した関数に配列をマッピングすることにより、パックされた配列を一般的なボックス配列に変換できundefinedます。この悪いケースはかなり簡単に回避できます。
  3. あなたはvec.add()あまりにも多くのタイプの物を渡して、それをメガモーフィックに変えることによってなど、関数全体を最適化解除するかもしれません。これは、「ジェネリックプログラミング」を実行する場合に発生する可能性vec.add()があります。これは、タイプに注意していない場合(多くのタイプが入っていることがわかる)と、最大のパフォーマンスを引き出したい場合の両方で使用されます。 (たとえば、ボックス化されたdoubleのみを受け取る必要があります)。

私の質問は、上記の考慮事項に照らして高性能のJavascriptコードを記述しながら、コードを見やすく読みやすく保つ方法についての、やさしい質問です。私が目指している答えの種類がわかるように、いくつかの特定のサブ質問:

  • (たとえば)パックされたSMI配列の世界にとどまりながらプログラミングする方法に関する一連のガイドラインはありますか?
  • マクロシステムのようなものを使用せずにJavaScriptで汎用の高性能プログラミングvec.add()を呼び出しサイトにインライン化することは可能ですか?
  • メガモーフィックな呼び出しサイトや最適化解除などの観点から、高性能のコードをライブラリにモジュール化する方法を教えてください。たとえば、線形代数パッケージAを高速で楽しく使用している場合B、に依存するパッケージをインポートしますがABそれを他のタイプで呼び出して最適化を解除すると、突然(コードを変更せずに)コードの実行が遅くなります。
  • 何か良いがある使いやすい JavaScriptエンジンがタイプで、内部でやっていることを確認するための測定ツールは?

1
これは非常に興味深いトピックであり、研究の一部を正しく行ったことを示す非常によく書かれた投稿です。しかし、私はこの質問がSO形式には広すぎるので、事実よりも多くの意見が必然的に集まることを恐れています。コードの最適化は非常に複雑な問題であり、エンジンの2つのバージョンが同じように動作しない場合があります。V8 JITの責任者はたまにぶら下がっている人もいると思うので、きちんとエンジンに答えてもらえるかもしれませんが、それでも1つのQ / Aでは広すぎると思います。
海洋堂

「私の質問は、高性能のJavascriptコードをどのように記述するかについての、やさしい質問です...」余談ですが、JavaScriptはバックグラウンドプロセス(Webワーカー)の生成を提供することに注意してください。 GPU(tensorflow.jsおよびgpu.js)の提供は、JavaScriptベースのアプリケーションの計算スループットを向上させるためにコンパイルのみに依存する以外の方法...
Jon Trent

@JonTrent実際、私は投稿で少し嘘をつきました。古典的な線形代数アプリケーションにはそれほど関心がなく、整数よりもコンピュータ代数に関心があります。これは、既存の数値パッケージの多くがすぐに除外されることを意味します。たとえば、行列を行縮小するときに2で割る可能性があるためです。は整数ではありません。私はWebワーカーを検討しました(特に、キャンセルできるようにしたいいくつかの長時間実行計算の場合)、ここで対処する問題は、対話に応答するのに十分な待機時間を短縮することです。
Joppy

JavaScriptでの整数演算の場合、おおまかに「|0すべての操作の背後にある」asm.jsスタイルのコードを見ていることでしょう。それはきれいではありませんが、適切な整数を持たない言語でできる最善のことです。BigIntsを使用することもできますが、現在のところ、一般的なエンジンのいずれでもそれほど高速ではありません(主に需要がないため)。
jmrk

回答:


8

V8開発者はこちら。この質問への関心の高さと他の答えの欠如を考えると、私はこれを試してみることができます。それはあなたが望んでいた答えではないのではないかと思います。

(たとえば)パックされたSMI配列の世界にとどまりながらプログラミングする方法に関する一連のガイドラインはありますか?

短い答え:それはここです:const guidelines = ["keep your integers small enough"]

より長い答え:包括的なガイドラインのセットを提供することは、さまざまな理由で困難です。一般に、私たちの意見では、JavaScript開発者は、開発者とそのユースケースにとって意味のあるコードを記述し、JavaScriptエンジン開発者は、そのコードをエンジンで高速に実行する方法を理解する必要があります。反対に、エンジンの実装の選択や最適化の取り組みに関係なく、一部のコーディングパターンは常に他のものよりも高いパフォーマンスコストを持つという意味で、その理想には明らかにいくつかの制限があります。

パフォーマンスのアドバイスについて話すときは、そのことを念頭に置き、どの推奨が多くのエンジンと何年にも渡って有効である可能性が高い可能性があり、また合理的に慣用的/非侵入的である推奨を慎重に推定します。

手元の例に戻ると、内部でSmisを使用することは、ユーザーコードが知る必要のない実装の詳細であることになっています。これにより、いくつかのケースがより効率的になり、他のケースでは害になりません。すべてのエンジンがSmisを使用するわけではありません(たとえば、AFAIK Firefox / Spidermonkeyは歴史的に使用していません。最近ではSmisを使用する場合があると聞きましたが、詳細がわからず、権限について話すことができません。問題)。V8では、Smisのサイズは内部の詳細であり、実際には時間の経過とともにバージョンが変更されています。大半のユースケースであった32ビットプラットフォームでは、Smisは常に31ビットの符号付き整数でした。64ビットプラットフォームでは、32ビットの符号付き整数でしたが、最近では最も一般的なケースのように思われましたが、Chrome 80で「ポインタ圧縮」が出荷されるまでは 64ビットアーキテクチャの場合、Smiサイズを32ビットプラットフォームで既知の31ビットに下げる必要がありました。Smisが通常32ビットであるという仮定に基づいて実装を行った場合、次のような不幸な状況が発生します。これ

ありがたいことに、お気づきのように、二重配列は依然として非常に高速です。数値の多いコードの場合、おそらくdouble配列を想定/対象とすることは理にかなっています。JavaScriptでのdoubleの普及を考えると、すべてのエンジンがdoubleおよびdouble配列を適切にサポートしていると想定するのが妥当です。

マクロシステムのようなものを使用してvec.add()のようなものを呼び出しサイトにインライン化せずに、JavaScriptで汎用の高性能プログラミングを実行することは可能ですか?

「ジェネリック」は一般に「ハイパフォーマンス」と対立します。これはJavaScriptや特定のエンジンの実装とは無関係です。

「汎用」コードは、実行時に決定を行う必要があることを意味します。関数を実行するたびに、たとえば「x整数であるかどうかを判断するためにコードを実行する必要があります。整数の場合、そのコードパスを使用します。x文字列ですか?次に、ここにジャンプします。オブジェクトですか.valueOf?ありますか?いいえ?次に多分.toString()?プロトタイプチェーンにいるかもしれませんか?それを呼び出し、最初からやり直してください」「高性能」に最適化されたコードは、基本的にこれらすべての動的チェックを削除するという考えに基づいて構築されています。これは、エンジン/コンパイラが事前に型を推測する方法を持っている場合にのみ可能です。それxが常に整数であることが証明できる(または十分に高い確率で想定できる)場合は、その場合にのみコードを生成する必要があります(証明されていない仮定が含まれていた場合は、型チェックによって保護されます)。

インライン化は、これらすべてに直交しています。「ジェネリック」関数は引き続きインライン化できます。場合によっては、コンパイラーは型情報をインライン関数に伝達して、そこでのポリモーフィズムを減らすことができます。

(比較のため:静的にコンパイルされた言語であるC ++には、関連する問題を解決するためのテンプレートがあります。簡単に言えば、特定の型でパラメーター化された関数(またはクラス全体)の特殊なコピーを作成するようにコンパイラーに明示的に指示することができます。それは場合によっては優れた解決策ですが、コンパイル時間が長い、バイナリが大きいなど、独自の欠点がないわけではありません。JavaScriptにはもちろんテンプレートなどはありません。これを使用evalして、多少似たシステムを構築することもできますが、 「同様の欠点にぶつかる:実行時にC ++コンパイラーと同等の作業を行う必要があり、生成するコードの量について心配する必要があります。)

メガモーフィックな呼び出しサイトや最適化解除などの観点から、高性能のコードをライブラリにモジュール化する方法を教えてください。たとえば、線形代数パッケージAを高速で快適に使用している場合、Aに依存するパッケージBをインポートしますが、Bが他のタイプでそれを呼び出して最適化を解除すると、突然(コードを変更せずに)コードの実行が遅くなります。

はい、それはJavaScriptの一般的な問題です。V8 Array.sortはJavaScriptに特定のビルトイン(など)を内部で実装していたため、この問題(「タイプフィードバック汚染」と呼ばれます)は、この手法から完全に離れた主な理由の1つでした。

とは言っても、数値コードの場合、それほど多くの型(Smisとdoubleのみ)はありません。実際に同様のパフォーマンスが得られるはずですが、型フィードバック汚染は確かに理論的な問題ですが、場合によっては大きな影響があり、線形代数シナリオでは測定可能な差が見られないこともかなりありそうです。

また、エンジンの内部には、「1つのタイプ==速い」や「複数のタイプ==遅い」よりも多くの状況があります。特定の操作でSmisとdoubleの両方が発生した場合は、それで問題ありません。2種類の配列からの要素の読み込みも問題ありません。「メガモーフィック」という用語は、負荷が非常に多くの異なるタイプを検出し、それらを個別に追跡することをあきらめている状況に使用します。代わりに、多数のタイプに適切にスケーリングするより一般的なメカニズムを使用します。このようなロードを含む関数は、それでも最適化されます。「非最適化」とは、以前には見られなかった新しい型が見られ、したがって最適化されたコードが処理できないため、関数の最適化されたコードを破棄しなければならないという非常に特殊な行為です。しかし、それでも問題ありません。最適化されていないコードに戻って、型フィードバックをさらに収集し、後で再度最適化するだけです。これが数回発生する場合、心配する必要はありません。病理学的に悪い場合にのみ問題になります。

つまり、すべてを要約すると、心配する必要ありません。適切なコードを記述し、エンジンで処理するだけです。そして、「合理的」とは、つまり、ユースケースにとって意味のあるもの、読み取り可能、保守可能、効率的なアルゴリズムを使用すること、配列の長さを超えて読み取るようなバグを含まないことです。理想的には、これですべてが終わり、他に何もする必要はありません。何かをしたほうがいいと感じた場合や、実際にパフォーマンスの問題を観察している場合は、2つのアイデアを提示できます。

TypeScript 使用すると役立ちます。重大な警告:TypeScriptの型は、実行パフォーマンスではなく開発者の生産性を目的としています(そして、結局のところ、これら2つの視点には型システムとは非常に異なる要件があります)。とはいえ、いくつかのオーバーラップがあります。たとえば、一貫してとして注釈を付けるとnumbernull数値のみを含む/操作するはずの配列または関数に誤って入力した場合、TSコンパイラーは警告を出します。もちろん、規律は依然として必要です。number_func(random_object as number)型注釈の正確性はどこにも強制されないため、1つのエスケープハッチがすべてを静かに弱体化する可能性があります。

TypedArraysの使用も役立ちます。通常のJavaScript配列と比較して、配列あたりのオーバーヘッド(メモリ使用量と割り当て速度)が少し多いため(多くの小さな配列が必要な場合は、通常の配列の方が効率的です)、拡張できないため、柔軟性が低くなります。または割り当て後に縮小しますが、すべての要素が1つの型を持つことを保証します。

Javascriptエンジンが型で内部的に行っていることをチェックするための使いやすい測定ツールはありますか?

いいえ、それは意図的なものです。上記で説明したように、V8が今日特に最適化できるパターンに合わせてコードを具体的に調整することは望んでおらず、実際にそれを実行したいとは思わない。その一連のことはどちらの方向にも変わる可能性があります。使用したいパターンがある場合は、将来のバージョンで最適化する可能性があります(以前は、ボックス化されていない32ビット整数を配列要素として格納するというアイデアを試してきました。 。しかし、その作業はまだ始まっていないので、約束はありません); また、過去に最適化に使用したパターンがある場合、他のより重要で影響の大きい最適化の邪魔になる場合は、そのパターンを削除することがあります。また、ヒューリスティックのインライン化などは、正しく行うことが難しいことで有名ですが、そのため、適切なタイミングで適切なインライン決定を行うことは、進行中の研究と、それに対応するエンジン/コンパイラの動作の変更の領域です。これは、これが皆にとって不幸な別のケースになります(あなたはそして私たち)あなたが現在のブラウザバージョンのいくつかのセットがおおよそあなたが考えている(または知っている)インライン展開の決定がほぼ完了するまでコードを微調整することに多くの時間を費やした場合、その当時の現在のブラウザを理解するために半年後に戻ってくるだけです彼らの発見的方法を変えた。

もちろん、アプリケーション全体のパフォーマンスを常に測定することができます。これは最終的に重要なことであり、特にエンジンが内部で行った選択ではありません。マイクロベンチマークには注意が必要です。誤解を招く可能性があります。コードを2行だけ抽出してベンチマークすると、シナリオが十分に異なり(たとえば、異なるタイプのフィードバック)、エンジンが非常に異なる決定を行う可能性があります。


2
この素晴らしい答えてくれてありがとう、それは物事がどのように動作するかについての私の疑惑の多くを確認し、それらをしている重要なのか意図仕事に。ちなみに、先ほどお話しした「型フィードバック」問題についてのブログなどはありますArray.sort()か?それについてもう少し読んでみたいです。
Joppy

私たちはその特定の側面についてブログを書いたとは思わない。それは本質的にあなたがあなた自身の質問であなたが説明したものです:組み込みがJavaScriptで実装されるとき、それらは「ライブラリのような」ものであり、異なるコードが異なるタイプでそれらを呼び出す場合、パフォーマンスが低下する可能性があります-ときどき、時々もっと。それだけではなく、間違いなくその手法の最大の問題すらありませんでした。私は主に私が一般的な問題に精通していると言いたかっただけです。
jmrk
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.