ホームページ > 記事 > ウェブフロントエンド > JavaScript の仕組みを理解し、V8 エンジンを詳しく理解し、最適化されたコードを作成します
関連する無料学習の推奨事項: javascript(ビデオ)
JavaScript エンジンは、JavaScript コードを実行するプログラムまたはインタープリターです。 JavaScript エンジンは、標準インタプリタとして、または JavaScript をバイトコードにコンパイルする何らかの形式のジャストインタイム コンパイラとして実装できます。
JavaScript エンジンを実装する人気のあるプロジェクトのリスト:
V8 は元々、Web ブラウザーでの JavaScript 実行のパフォーマンスを向上させるために設計されました。速度を上げるために、V8 ではインタープリターを使用する代わりに、JavaScript コードをより効率的なマシン コードに変換します。 SpiderMonkey や Rhino (Mozilla) などの多くの最新の JavaScript エンジンと同様に、JIT (ジャストインタイム) コンパイラーを実装することで、JavaScript コードを実行時マシン コードにコンパイルします。ここでの主な違いは、V8 はバイトコードや中間コードを生成しないことです。
V8 には 2 つのコンパイラがありました
V8 の 5.9 バージョンがリリースされる前、V8 エンジンは 2 つのコンパイラを使用していました:
full-codegen — 1 つ シンプルで非常に高速シンプルで比較的遅いマシンコードを生成するコンパイラ。 クランクシャフト - 高度に最適化されたコードを生成する、より洗練された (ジャストインタイム) 最適化コンパイラー。隠しクラス
JavaScript はプロトタイプベースの言語です。クラスとオブジェクトはクローン作成プロセスを使用して作成されません。 JavaScript は動的プログラミング言語でもあります。つまり、インスタンス化後にオブジェクトにプロパティを簡単に追加したり、オブジェクトからプロパティを削除したりできます。
ほとんどの JavaScript インタープリターは、辞書のような構造 (ハッシュ関数に基づく) を使用して、オブジェクト プロパティ値のメモリ内の位置を保存します。この構造により、JavaScript でのプロパティ値の取得が Java や C# より高速になります。非動的プログラミング言語ではコストが高くなります。 Java では、すべてのオブジェクト プロパティはコンパイル前の固定オブジェクト レイアウトによって決定され、実行時に動的に追加または削除することはできません (もちろん、C# には動的型付けがあり、これについては別のトピックになります)。したがって、属性値 (またはこれらの属性へのポインタ) は、各バッファ間の固定オフセットを使用して、連続したバッファとしてメモリに保存できます。オフセットの長さは属性タイプに基づいて簡単に決定できますが、 JavaScript では不可能な、実行時にプロパティ タイプを変更できます。
辞書を使用してメモリ内のオブジェクトのプロパティの場所を見つけるのは非常に効率が悪いため、V8 では別のアプローチ、つまり隠しクラスを使用します。非表示クラスは、実行時に作成される点を除けば、Java などの言語で使用される固定オブジェクト (クラス) と同様に機能します。さて、実際の例を見てみましょう:
「new Point(1,2)」呼び出しが発生すると、V8 は「C0」という名前の付いた非表示の種類を作成します。
Point にはプロパティが定義されていないため、「C0」は空です。
最初のステートメント "this.x = x" が ("Point" 関数内で) 実行されると、V8 は "C0" に基づく "C1" という名前の 2 番目の隠しクラスを作成します。 「C1」は、プロパティ x が見つかるメモリ内の位置 (オブジェクト ポインタを基準とした) を表します。
この場合、 "x" はオフセット 0 に格納されます。つまり、メモリ内のポイント オブジェクトを連続バッファーとみなすと、最初のオフセットが属性 "x" に対応します。また、V8 では、属性「x」がポイント オブジェクトに追加された場合、隠しクラスが「C0」から「C1」に切り替わる必要があることを示す「クラス変換」で「C0」を更新します。以下の点オブジェクトの非表示クラスは「C1」になりました。
#新しいプロパティがオブジェクトに追加されるたびに、古い非表示クラスは、新しい非表示クラスを指す変換パスで更新されます。非表示クラスのキャストは、同じ方法で作成されたオブジェクト間で非表示クラスを共有できるため、重要です。 2 つのオブジェクトが非表示クラスを共有し、同じプロパティがそれらに追加される場合、変換により、両方のオブジェクトが同じ新しい非表示クラスとそれに付属するすべての最適化コードを確実に受け取ります。
ステートメント「this.y = y」が実行されるとき (「this.x = x」ステートメントの後の「Point」関数内)、同じプロセスが繰り返されます。
「C2」という名前の新しい非表示クラスが作成されます。プロパティ「y」が Point オブジェクト (プロパティ「x」がすでに含まれている) に追加される場合、クラス変換が「C1」に追加されます。 " の場合、非表示クラスを「C2」に変更し、ポイント オブジェクトの非表示クラスを「C2」に更新する必要があります。
#隠しクラスの変換は、プロパティがオブジェクトに追加される順序によって異なります。次のコード スニペットを見てください。 ここで、p1 と p2 に同じ非表示クラスと変換が使用されると仮定します。したがって、「p1」には、最初に属性「a」を追加し、次に属性「b」を追加します。ただし、「p2」には最初に「b」が割り当てられ、次に「a」が割り当てられます。したがって、「p1」と「p2」は、変換パスが異なるため、最終的に異なる非表示カテゴリになります。この場合、隠しクラスを再利用できるように、同じ順序で動的プロパティを初期化する方がはるかに適切です。 インライン キャッシュV8 は、インライン キャッシュと呼ばれる動的型付け言語を最適化するための別の手法を利用します。インライン キャッシュは、同じメソッドの繰り返し呼び出しが同じ種類のオブジェクトで発生する傾向があるという観察に基づいています。インライン キャッシュの詳細な説明は、ここでご覧いただけます。 インライン キャッシュの一般的な概念については、次に説明します (上記を深く理解する時間がない場合)。 それでは、どのように機能するのでしょうか? V8 は、最近のメソッド呼び出しで引数として渡されたオブジェクト タイプのキャッシュを維持し、この情報を使用して、将来引数として渡されるオブジェクト タイプを予測します。 V8 がメソッドに渡されるオブジェクトのタイプを十分に予測できれば、オブジェクトのプロパティにアクセスする方法のプロセスをバイパスし、代わりにオブジェクトの非表示クラスへの以前の検索で保存された情報を使用できる可能性があります。 それでは、隠しクラスとインライン キャッシュの概念はどのように関係しているのでしょうか?特定のオブジェクトでメソッドが呼び出されるたびに、V8 エンジンはそのオブジェクトの非表示クラスの検索を実行して、特定のプロパティにアクセスするオフセットを決定する必要があります。同じ隠しクラスへの呼び出しが 2 回成功すると、V8 は隠しクラスの検索を省略し、プロパティのオフセットをオブジェクト ポインター自体に単純に追加します。このメソッドへの次回の呼び出しでは、V8 エンジンは非表示クラスが変更されていないと想定し、前の検索で保存されたオフセットを使用して特定のプロパティのメモリ アドレスに直接ジャンプします。これにより、実行速度が大幅に向上します。インライン キャッシュは、同じタイプのオブジェクトが非表示クラスを共有することが重要である理由でもあります。同じタイプで異なる非表示クラスの 2 つのオブジェクトを作成する場合 (前の例で行ったように)、2 つのオブジェクトが同じタイプであっても、対応する非表示クラスはそのオブジェクトに対応するため、V8 はインライン キャッシュを使用できません。プロパティには異なるオフセットが割り当てられます。
2 つのオブジェクトは基本的に同じですが、「a」プロパティと「b」プロパティは異なる順序で作成されます。
水素グラフが最適化されると、クランクシャフトはそれをリチウムと呼ばれる下位レベルの表現に縮小します。ほとんどの Lithium 実装はアーキテクチャ固有です。レジスタ割り当ては、多くの場合、このレベルで発生します。
最後に、Lithium はマシンコードにコンパイルされます。次に、OSR: オンスタック置換です。明示的な長時間実行メソッドのコンパイルと最適化を開始する前に、スタックの置換を実行することがあります。 V8 は、スタックの置換をゆっくりと実行して再び最適化を開始するだけではありません。代わりに、実行中にすべてのコンテキスト (スタック、レジスタ) を変換し、最適化されたバージョンに切り替えます。 V8 では最初にコードがインライン化されることを考慮すると、これは非常に複雑なタスクです。それが可能なエンジンはV8だけではありません。
逆最適化と呼ばれる安全対策があり、エンジンが無効であると想定して逆の変換を行い、最適化されていないコードを返します。
ガベージ コレクションの場合、V8 は従来のマーク アンド スイープ アルゴリズムを使用して古い世代をクリーンアップします。マーキングフェーズでは JavaScript の実行を停止する必要があります。 GC コストを制御し、実行をより安定させるために、V8 は増分マーキングを使用します。つまり、ヒープ全体をウォークしてすべての可能なオブジェクトをマークしようとするのではなく、ヒープの一部をウォークして通常の実行を再開します。次の GC 停止は、前回のヒープ ウォークが中断したところから継続されます。これにより、通常の実行中に非常に短い一時停止が可能になります。前述したように、スキャン フェーズは別のスレッドによって処理されます。
2017 年初めの V8 5.9 のリリースで、新しい実行パイプラインが導入されました。この新しいパイプラインにより、実際の JavaScript アプリケーションのパフォーマンスが大幅に向上し、メモリが大幅に節約されます。
新しい実行フローは、Ignition (V8 のインタープリター) と TurboFan (V8 の最新の最適化コンパイラー) に基づいて構築されています。
V8 5.9 のリリース以来、V8 チームは新しい JavaScript 言語機能とこれらの機能に必要な最適化に追いつくのに苦労してきたため、V8 チームは完全なコード生成とクランクシャフトを使用しなくなりました (2010 年以降) V8 テクノロジーによってサービスされます)。
これは、V8 が全体的によりシンプルで保守しやすいアーキテクチャになることを意味します。
#これらの改善はほんの始まりにすぎません。新しい Ignition パイプラインと TurboFan パイプラインは、今後何年にもわたって JavaScript のパフォーマンスを向上させ、Chrome と Node.js における V8 のフットプリントを縮小するさらなる最適化への道を開きます。関連する無料学習の推奨事項:php プログラミング (ビデオ)
以上がJavaScript の仕組みを理解し、V8 エンジンを詳しく理解し、最適化されたコードを作成しますの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。