V8内部メカニズムとオブジェクトプロパティの迅速な作業

この資料では、V8の内部メカニズムがJavaScriptオブジェクトのプロパティとどのように機能するかに焦点を当てています。 JavaScriptの観点からプロパティを検討する場合、それらの異なるタイプは互いにそれほど異なりません。 JSオブジェクトは通常、文字列キーと任意のオブジェクトを値として持つ辞書のように動作します。 ただし、言語仕様を読むと、たとえば、列挙されたときに異なるタイプのプロパティの動作が異なることがわかります。 他の場合では、さまざまな種の特性の振る舞いは基本的に同じに見えます。



プロパティの類似性を考慮したプロパティの操作メカニズムの実装は、それほど大規模なタスクではないように見えますが、V8腸では、プロパティを表すいくつかの異なる方法が使用されます。 これは、最初に高いパフォーマンスを確保するために行われ、次にメモリを節約するために行われます。



画像



この記事では、オブジェクトの動的に追加されたプロパティを処理するときにV8がどのように高いパフォーマンスを実現するかについて説明します。 V8でJavaScriptの実行を最適化する方法の本質、例えば組み込みキャッシュなどを理解するには、プロパティを操作するメカニズムの機能の知識が必要です。



ここでは、V8が名前付きプロパティと整数でインデックス付けされたプロパティを処理する方法について説明します。 その後、新しい名前付きプロパティをオブジェクトに追加するときに、非表示クラスの機能の特徴を考慮します。これにより、オブジェクトの形状をすばやく識別できます。 次に、V8の内部メカニズムについての話を続け、非表示のプロパティを使用する機能に応じて、目的の最適化を表示します。 最後のセクションを確認した後、整数がインデックス付けされたプロパティまたはインデックスが割り当てられた配列要素をV8がどのように処理するかを学習します。



名前付きプロパティと配列要素の比較



非常に単純なオブジェクトを分析することから始めましょう。 たとえば、 {a: "foo", b: "bar"}



ます。 このオブジェクトには、 b



b



という2つの名前付きプロパティa



あります。 このオブジェクトには、プロパティ名の整数インデックスがありません。 より一般的に要素として知られるインデックス付きプロパティは、配列の特性です。 たとえば、配列["foo", "bar"]



は2つのインデックス付きプロパティがあります0



は値foo



で、 1



は値bar



です。 V8で名前付きプロパティとインデックス付きプロパティを表す実装の最初の主要な違いについて説明しました。



次の図は、通常のJavaScriptオブジェクトがメモリ内でどのように見えるかを示しています。









名前付きおよびインデックス付きプロパティ



要素とプロパティはさまざまなデータ構造に保存されます。 これにより、新しいプロパティと要素を追加したり、それらを操作するためのさまざまなテンプレートでそれらにアクセスしたりする操作の効率が向上します。



要素は、主にpop



slice



などのさまざまなArray.prototypeメソッドに使用されslice



。 これらの関数が次々に続くプロパティで機能することを考えると、V8の内部表現は、ほとんどの場合、単純な配列のように見えます。



後で、メモリを節約するためにインデックス付きプロパティを保存するための辞書メカニズムの使用に切り替える状況について説明します。 特に、スパース配列を辞書に置き換えることについて話しています。



名前付きプロパティは同様に別の配列に保存されます。 ただし、要素とは異なり、キーを使用してプロパティストア内の位置を見つけることはできません。 追加のメタデータが必要です。 V8では、すべてのJavaScriptオブジェクトに隠しクラス(HiddenClass)が関連付けられています。 隠しクラスには、オブジェクトの形状に関する情報、特にプロパティ名とプロパティストア内のインデックスの対応に関する情報が格納されます。 複雑な作業シナリオでは、単純な配列ではなく辞書を使用してプロパティを保存することがあります。 対応するセクションでは、これについてさらに詳しく説明します。



▍結論





非表示のクラスと記述子配列



要素と名前付きプロパティの主な違いが何であるかを理解したら、V8で非表示のクラスがどのように機能するかを調べる必要があります。



非表示のクラスには、オブジェクトのプロパティの数やプロトタイプへのリンクなど、オブジェクトに関するメタ情報が格納されます。 隠しクラスは、典型的なオブジェクト指向プログラミング言語のクラスと概念的に似ています。 ただし、JavaScriptなどのプロトタイプベースの言語では、通常、オブジェクトクラスについて事前に知ることはできません。 その結果、この場合V8では、非表示のクラスがオンザフライで作成され、オブジェクトが更新されると動的に更新されます。



非表示のクラスは、オブジェクトの形状の識別子として機能するため、最適化V8コンパイラと組み込みキャッシュメカニズムの非常に重要な部分です。 たとえば、最適化コンパイラは、隠しクラスとオブジェクトの構造の互換性を保証できる場合、適切なデータ構造にプロパティ値を埋め込むことを利用できます。



隠されたクラスの重要な部分を見てください。









JSオブジェクト、非表示クラス、および名前付きプロパティに関する情報を含む記述子



V8では、JSオブジェクトの最初のフィールドは非表示のクラスを指します。 (実際には、これはV8ヒープ上にあり、ガベージコレクターによって管理されるオブジェクトの場合です)。 プロパティを操作するという観点から見ると、最も重要なのは、 bit field 3



として図に示されているbit field 3



であり、プロパティの数と記述子の配列へのポインタが格納されています。 記述子配列には、名前付きプロパティに関する情報、特にプロパティの名前と値が格納される位置が含まれます。 ここでは整数でインデックス付けされたプロパティでは動作しないため、記述子配列には対応するエントリがないことに注意してください。



オブジェクトに非表示クラスを割り当てると、V8は同じ構造を持つオブジェクト、つまり同じ名前のプロパティが同じ順序であるオブジェクトが同じ非表示クラスを持つという仮定から進みます。 これを実現するために、新しいプロパティがオブジェクトに追加されると、新しい非表示クラスがそれに割り当てられます。 次の例では、空のオブジェクトから始めて、3つの名前付きプロパティを追加します。









オブジェクトに名前付きプロパティを追加するときに中間の非表示クラスを作成する



新しいプロパティが追加されるたびに、オブジェクトの非表示のクラスが変更されます。 V8は、非表示のクラスを接続する遷移ツリーを作成します。 V8は、たとえば、プロパティa



を空のオブジェクトに追加するときに、どの隠しクラスを取るかを知っています。 この遷移ツリーにより、オブジェクトが同じ方法で配置されたときに、同じ隠しクラスを確実に受け取ることができます。



次の例は、単純なインデックス可能なプロパティがオブジェクトに追加されても、遷移ツリーが同じになることを示しています。









オブジェクトに名前付きおよびインデックス付きプロパティを追加する



ただし、他の名前付きプロパティ(この場合はd



)が追加された新しいオブジェクトを作成すると、V8は新しい非表示クラス用に別のブランチを作成します。









異なるプロパティセットを持つオブジェクトのさまざまな遷移ツリーの構築



▍結論





3種類の名前付きプロパティ



V8がオブジェクトの形状に関する情報をサポートするために隠しクラスを使用する方法を説明した後、名前付きプロパティが実際に保存される方法について説明します。 上に示したように、プロパティには2つの基本的な種類があります。名前付きとインデックス付きです。 ここでは、名前付きプロパティについて詳しく説明します。



{a: 1, b: 2}



などの単純なオブジェクトは、V8では異なる内部表現を持つことができます。 JSオブジェクトの動作は単純な辞書の動作に多少似ているように見えるかもしれませんが、V8はそれらを辞書の形式で表現することを避けようとします。



objectsオブジェクトの内部プロパティと通常のプロパティの比較



V8は、オブジェクト自体に直接保存されるオブジェクトのいわゆる内部プロパティをサポートします。 これらは、追加のアクションを実行せずにアクセスできるため、V8で使用される最速のプロパティです。 オブジェクトの内部プロパティの数は、オブジェクトの初期サイズによって決まります。 オブジェクトのスペースが許可するよりも多くのプロパティが追加される場合、プロパティストアに配置されます。 プロパティストアは、抽象化のレイヤーを追加しますが、オブジェクトに関係なくサイズを増やすことができます。









最速で作業するプロパティの数は、オブジェクトの初期サイズによって事前に決定されます。 プロパティ値もかなり迅速に処理され、単純なプロパティ配列に保​​存されます



fast高速プロパティと低速プロパティの比較



次に注意すべき重要なことは、「高速」プロパティと「低速」プロパティの違いです。 通常、線形プロパティストアに格納される「高速」プロパティを呼び出します。 このようなプロパティは、リポジトリ内のインデックスによってアクセスされます。 プロパティの名前からリポジトリ内の位置に移動するには、上記のように、記述子の配列を参照する必要があります。









プロパティのディクショナリは自己完結型であり、それを操作する場合、記述子配列からの追加のメタ情報は必要ありません



ただし、オブジェクトのプロパティを追加および削除する操作が多数ある場合、記述子の配列と非表示クラスをサポートするには、追加の時間とメモリが必要になる場合があります。 したがって、V8はさらに、いわゆるスロープロパティをサポートします。 遅いプロパティを持つオブジェクトは、自己完結型の辞書をプロパティストアとして使用します。 すべてのプロパティメタ情報は、非表示クラスの記述子の配列に保存されるのではなく、プロパティディクショナリに直接配置されます。 その結果、非表示のクラスを更新せずにプロパティを追加および削除できます。 ビルトインキャッシュは、ディクショナリに格納されているプロパティでは機能しないため、通常、そのようなプロパティでの作業は「高速」プロパティでの作業よりも遅くなります。



▍結論





  1. オブジェクトの内部プロパティはオブジェクトに直接保存され、それらを操作するのが最も高速です。

  2. クイックプロパティはプロパティストアに配置され、そのメタ情報は非表示クラスの記述子の配列に保存されます。

  3. 遅いプロパティは自己完結型のプロパティディクショナリに格納されますが、そのメタ情報は他の非表示のクラス構造には格納されなくなりました。





要素またはインデックス付きプロパティ



これまで、名前付きプロパティについて説明しましたが、今度は整数でインデックス付けされたプロパティを処理するときです。整数は通常、配列を操作するときに使用されます。 このようなプロパティのサポートは、名前付きプロパティのサポートと同じくらい複雑です。 インデックス付きプロパティは常に要素の個別のリポジトリに配置されますが、 20種類の要素があるという事実を複雑にします!



▍要素のソリッド配列およびスパース配列



配列要素の操作方法の最初の大きな違いは、ソリッド配列またはスパース配列のどちらをストレージとして使用するかです。 索引付けされたアイテムが削除されたとき、または定義されていないアイテムがある場合など、リポジトリに空のスペースまたは「穴」が表示されます。 「穴」のある配列の簡単な例は[1,,3]



,, [1,,3]



です。 この場合、配列には2番目の要素はありません。 次の例は、この問題を示しています。



 const o = ["a", "b", "c"]; console.log(o[1]);          //  "b". delete o[1];                //     «». console.log(o[1]);          //  "undefined";  1  . o.__proto__ = {1: "B"};     //   1  . console.log(o[0]);          //  "a". console.log(o[1]);          //  "B". console.log(o[2]);          //  "c". console.log(o[3]);          //  undefined
      
      











スパース配列を使用してアイテムを保存する際の問題



これを一言で説明すると、アクセスしているオブジェクトにプロパティが含まれていない場合、プロトタイプチェーンを通過する必要があることがわかります。 配列の要素が自給自足である、つまり、既存のインデックス付きプロパティに関する情報を非表示クラスに保存しない場合、存在しない値をマークするthe_hole



と呼ばれる特別な値が必要です。 これは、 Array



オブジェクト関数のパフォーマンスに非常に悪い影響を与えます。 リポジトリに「穴」がないこと、つまり要素リポジトリに配列の欠損値に関する情報が含まれていないことがわかっている場合、プロトタイプチェーンでの低速検索を必要とせずにローカル操作を実行できます。



▍クイックおよびボキャブラリー要素



配列の要素を分離できる次の兆候は、内部表現に応じて、要素を操作する速度です。 「遅い」アイテムは辞書に保存されます。 「高速」要素の操作は、仮想マシンの通常の内部アレイを使用して実行されます。 ここで、アイテムインデックスはアイテムストアのインデックスにマップされます。 ただし、そのような単純な配列の表現は、比較的少数のセルしか占有されていない非常に大きなスパース配列には不経済すぎます。 そのような場合、辞書ベースの配列表現を使用します。 これにより、アイテムへのアクセスが遅くなりますが、メモリが節約されます。



 const sparseArray = []; sparseArray[9999] = "foo"; //  ,     
      
      





この例では、10,000エントリの配列にメモリを割り当てると、メモリ使用量の面でかなり無駄になります。 代わりに、V8は配列--



のトリプレットが格納される配列を作成します。 この場合のキーは9999



で、値はfoo



および標準記述子です。 さらに、隠しクラスに記述子の詳細を格納する方法がないため、V8は独自の記述子でインデックス付きプロパティを設定するたびに、要素を格納する低速な方法を使用することに注意する必要があります。



 const array = []; Object.defineProperty(array, 0, {value: "fixed", configurable: false}); console.log(array[0]);      //  "fixed". array[0] = "other value";   //      0. console.log(array[0]);      //   "fixed".
      
      





この例では、構成不可能な要素を配列に追加しました。 この情報は、記述子に関連する遅い語彙要素のトリプレットのその部分に格納されます。 Array



オブジェクトの機能は、要素が辞書に保存されている配列では非常に遅くなることに注意することが重要です。



▍SmiおよびDouble Elements



V8では、高速要素はさらに別の機能によって区切られています。 たとえば、 Array



型のオブジェクトに整数のみを保存し、これが頻繁に発生する場合、整数はいわゆる小整数(小整数、smi)に直接エンコードされるため、ガベージコレクターは配列を解析する必要がありません。 別の特殊なケースは、倍精度の数値のみを含む配列です。 小さな整数とは異なり、浮動小数点数は通常、数ワードを占める整数オブジェクトとして表されます。 ただし、V8は通常の倍精度数をDouble



型の配列の形式で保存し、不必要なメモリ負荷を回避し、不必要な計算でコンピューターを占有しないようにします。 次の例は、Smi要素とDouble要素を持つ配列の4つのオプションを示しています。



 const a1 = [1,   2, 3];  // Smi,   const a2 = [1,    , 3];  // Smi,  ,     a2[1]   const b1 = [1.1, 2, 3];  // Double,   const b2 = [1.1,  , 3];  // Double,  ,     b2[1]  
      
      





other他の種類の要素



上記で説明したことにより、20種類のうち7種類の配列要素を記述することができました。 話を複雑にしないために、型付き配列の9種類の要素と、文字列ラッパーの2種類の要素については説明しませんでした。 また、引数オブジェクトの2種類の特別な要素については説明しませんでした。 それらは最後に言及しましたが、他のタイプの要素と同じくらい重要です。



▍ElementAccessor



要素の型の数に応じて、 Array



オブジェクトのすべての関数をC ++で20回書き換えることをあまり望んでいないことは十分に理解できると思います。 ここで、C ++の特別な機能の一部が表示されます。 Array



オブジェクトに多くの関数を作成する代わりに、主にストアから要素にアクセスする単純な関数のみを実装する必要があるElementAccessor



を作成しました。



ElementAccessor



は、 CRTP手法を使用して、 Array



オブジェクトの関数の特殊バージョンを作成します。 したがって、配列のslice



メソッドのようなものを呼び出すと、C ++で記述された組み込みメカニズムがV8でアクティブになり、 ElementAccessor



を介して関数の特殊バージョンへの移行が実行されます。









特定の要素ビュー用に最適化された要素ベースのコール転送とカスタム実装



▍結論





まとめ



V8でプロパティがどのように機能するかを理解することは、多くの最適化の鍵です。 JS開発者は、ここで説明するメカニズムと直接対話しません。 ただし、V8でプロパティを操作する方法を知っていると、一部の開発テンプレートが他のテンプレートよりも高速なコードを提供する理由を理解するのに役立ちます。 たとえば、通常、オブジェクトまたは配列の要素のプロパティタイプを変更すると、V8が新しい非表示クラスを作成するため、タイプの「詰まり」を引き起こし、V8が最適化されたコードを生成できなくなります。



親愛なる読者! JS-codeのパフォーマンスが理解できないほど低下しましたが、この資料を使用して説明および修正できますか?



All Articles