まえがき
Daniel Cliffordは、V8エンジン用のJavaScriptコードを最適化する機能について、Google I / Oで素晴らしい講演を行いました。 ダニエルは、高速化に努め、C ++とJavaScriptの違いを注意深く分析し、インタープリターがどのように機能するかを覚えてコードを書くことを推奨しました。 この記事では、ダニエルのパフォーマンスの最も重要なポイントの要約を収集しました。エンジンの変更に合わせて更新します。
最も重要なアドバイス
コンテキストでパフォーマンスのヒントを提供することは非常に重要です。 最適化は多くの場合、強迫観念になり、ジャングルに深く没頭すると、実際にはより重要なことから注意をそらすことができます。 Webアプリケーションのパフォーマンスを総合的に把握する必要があります。これらの最適化のヒントに焦点を合わせる前に、 PageSpeedなどのツールを使用してコードを分析し、最初に全体的に良い結果を得る必要があります。 これにより、時期尚早な最適化を回避できます。
高速なWebアプリケーションを構築するための最良の戦略は次のとおりです。
- 問題が発生する前に考え直してください。
- 問題の核心を理解してください。
- 重要なことだけを修正してください。
この戦略を順守するには、V8がJSを最適化する方法を理解し、実行時にすべてがどのように発生するかを想像することが重要です。 適切なツールを持つことも重要です。 ダニエルはスピーチの中で、開発者ツールにもっと時間を割きました。 この記事では、主にV8アーキテクチャーの機能に注目します。
それでは始めましょう。
非表示のクラス
コンパイル段階では、JavaScriptの型に関する情報は非常に限られています。型は実行時に変更される可能性があるため、コンパイル中に型について推測することは難しいと予想するのは自然です。 問題が発生します-そのような状況で、どのようにしてC ++の速度に近づくことができますか? ただし、V8は実行時にオブジェクトの非表示クラスを作成します。 同じクラスを持つオブジェクトは、同じ最適化されたコードを共有します。
例:
function Point(x, y) { this.x = x; this.y = y; } var p1 = new Point(11, 22); var p2 = new Point(33, 44); // p1 p2 p2.z = 55; // ! p1 p2 !
「
.z
」プロパティが
p2
に追加されるまで、コンパイラ内の
p1
と
p2
は同じ隠しクラスを持ち、V8は両方のオブジェクトに同じ最適化されたマシンコードを使用できました。 隠しクラスを変更する頻度が少ないほど、パフォーマンスが向上します。
結論:
- コンストラクター内のすべてのオブジェクトを初期化して、将来可能な限り変更しないようにします。
- オブジェクトのプロパティは常に同じ順序で初期化します。
数字
V8は、変数の使用方法を追跡し、各タイプに対して最も効率的な表現を使用します。 型の変更は非常に高価になる可能性があるため、浮動小数点数と整数を混在させないようにしてください。 一般に、整数を使用することをお勧めします。
例:
var i = 42; // 31- var j = 4.2; //
結論:
- 可能な限り31ビットの符号付き整数を使用してください。
配列
V8は、配列の2種類の内部表現を使用します。
- コンパクトな順次キーセット用の実配列。
- その他の場合のハッシュテーブル。
結論:
- 配列をあるカテゴリから別のカテゴリに強制的にジャンプさせないでください。
- 0から始まる連続したインデックス番号を使用します(Cとまったく同じです)。
- 大きな配列(64K以上の要素を含む)を事前に入力しないでください-これは何もしません。
- 配列(特に数値要素)から要素を削除しないでください。
- 初期化されていないアイテムや削除されたアイテムにはアクセスしないでください。 例:
a = new Array(); for (var b = 0; b < 10; b++) { a[0] |= b; // ! } //vs. a = new Array(); a[0] = 0; for (var b = 0; b < 10; b++) { a[0] |= b; // ! . }
倍精度の数値の配列は最も速く動作します-数値はオブジェクトとしてではなく基本型として展開されて保存されます。 配列の軽率な使用は、頻繁にアンパックする可能性があります。
var a = new Array(); a[0] = 77; // a[1] = 88; a[2] = 0.5; // , a[3] = true; // ,
次のようにはるかに高速になります。
var a = [77, 88, 0.5, true];
最初の例では、個々の割り当てが連続して発生し、a[2]
が値を取得した瞬間に、コンパイラーは倍精度でパックされていない数値の配列に変換a
a[3]
非数値要素で初期化されると、逆変換が発生します。 2番目の例では、コンパイラは目的の配列タイプをすぐに選択します。
このように:
- 小さな固定配列は、配列リテラルを使用して初期化するのが最適です。
- 使用する前に小さな配列(<64K)を埋めてください。
- 数値配列に非数値を格納しないでください。
- リテラルを使用せずに初期化するときは、変換を避けるようにしてください。
JavaScriptのコンパイル
JavaScriptは動的言語であり、元々は解釈されていましたが、最新のエンジンはすべてコンパイラーです。 V8では2つのコンパイラーが同時に動作します。
- スクリプト全体のコードを生成するベースコンパイラ。
- 最もホットなサイト向けの非常に高速なコードを生成する最適化コンパイラ。 このコンパイルには時間がかかります。
ベースコンパイラ
V8では、最初にすべてのコードの処理を開始し、可能な限り迅速に実行します。 それによって生成されたコードはほとんど最適化されていません-基本的なコンパイラは型についてほとんど仮定をしません。 実行中、コンパイラはコードのタイプ依存セクションが格納されるインラインキャッシュを使用します。 このコードが再起動されると、コンパイラーは、キャッシュから既製コードの適切なバージョンを選択する前に、使用されているタイプをチェックします。 したがって、異なるタイプで作業できる演算子は実行が遅くなります。
結論:
- 多相演算子よりも単相演算子を優先します。
演算子は、オペランドの非表示タイプが常に同じ場合は単相であり、変更できる場合は多相です。 たとえば、
add()
2番目の呼び出しにより、コードはポリモーフィックになります。
function add(x, y) { return x + y; } add(1, 2); // + add() add("a", "b"); // + add()
コンパイラの最適化
基本コンパイラの作業と並行して、最適化コンパイラは、頻繁に実行されるコードの「ホット」セクションを再コンパイルします。 インラインキャッシュに蓄積された型情報を使用します。
最適化コンパイラは、呼び出し場所に関数を埋め込みます。これにより、実行が高速化されますが(メモリ消費が増加します)、追加の最適化が可能になります。 単相関数とコンストラクターは簡単に完全に組み込むことができます。これは、それらを使用するよう努力する必要があるもう1つの理由です。
d8エンジンのスタンドアロンバージョンを使用して、コードで正確に最適化されているものを確認できます。
(最適化された関数の名前はd8 --trace-opt primes.js
stdout
に表示されます)
すべての機能を最適化できるわけではありません。 特に、最適化コンパイラは、
try/catch
ブロックを含む関数をスキップします。
結論:
try/catch
を使用する必要がある場合は、パフォーマンスが重要なコードを外部に置きます。 例:
function perf_sensitive() { // } try { perf_sensitive() } catch (e) { // }
おそらく将来的には状況が変わり、最適化コンパイラーで
try/catch
ブロックをコンパイルできるようになるでしょ
try/catch
。 d8の起動時に
--trace-bailout
指定すると、どの関数が無視されるかを正確に確認できます。
d8 --trace-bailout primes.js
最適化解除
最適化コンパイラによって生成されるコードは、常に高速であるとは限りません。 この場合、最適化されていない元のバージョンが使用されます。 最適化に失敗したコードは破棄され、ベースコンパイラによって作成されたコード内の適切な場所から実行が継続されます。 状況が許せば、おそらくこのコードはすぐに再び最適化されるでしょう。 特に、すでに最適化されたコード内の非表示のクラスを変更すると、最適化が解除されます。
結論:
- 最適化された関数の非表示クラスを変更しないでください。
--trace-deopt
を指定してd8を実行すると、どの関数が最適化解除されているかを正確に
--trace-deopt
。
d8 --trace-deopt primes.js
その他のV8ツール
上記の機能は、起動時にGoogle Chromeに転送できます。
/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" --js-flags="--trace-opt --trace-deopt --trace-bailout
d8にはプロファイラーもあります。
d8 primes.js --prof
d8サンプリングプロファイラーは、ミリ秒ごとにスナップショットを
v8.log
し、
v8.log
書き込みます。
まとめ
適切に最適化されたコードを記述するためには、V8エンジンの動作を理解することが重要です。 また、記事の冒頭で説明した一般原則を忘れないでください。
- 問題が発生する前に考え直してください。
- 問題の核心を理解してください。
- 重要なことだけを修正してください。
つまり、PageSpeedなどのツールを使用して、JavaScript内にあることを確認する必要があります。 ボトルネックを探す前に、DOM呼び出しを取り除く価値があるかもしれません。 ダニエルのプレゼンテーション(およびこの記事)がV8の動作をよりよく理解するのに役立つことを願っていますが、特定のエンジンに合わせて調整するよりも、プログラムアルゴリズムを最適化する方が便利であることを忘れないでください。