JavaScriptエンジンの基本:プロトタイプの最適化。 パート2

こんにちは、友達! 「情報システムのセキュリティ」コースが開始されました。これに関連して、記事「JavaScriptエンジンの基礎:プロトタイプの最適化」の最後の部分を共有します。



また、現在の出版物は次の2つの記事の続きであることを思い出してください。 「JavaScriptエンジンの基礎:一般的なフォームとインラインキャッシュ。 パート1 " 、 " JavaScriptエンジンの基本:一般的なフォームとインラインキャッシュ。 パート2」







クラスとプロトタイププログラミング



JavaScriptオブジェクトのプロパティにすばやくアクセスする方法がわかったので、JavaScriptクラスのより複雑な構造を見てみましょう。 JavaScriptのクラス構文は次のようになります。



class Bar { constructor(x) { this.x = x; } getX() { return this.x; } }
      
      





これはJavaScriptの比較的新しい概念のように見えますが、JavaScriptで常に使用されているプロトタイププログラミングの「構文糖」にすぎません。



 function Bar(x) { this.x = x; } Bar.prototype.getX = function getX() { return this.x; };
      
      





ここでは、 getX



プロパティをgetX



オブジェクトにgetX



ます。 JavaScriptのプロトタイプは同じオブジェクトであるため、これは他のオブジェクトと同じように機能します。 JavaScriptなどのプロトタイププログラミング言語では、メソッドはプロトタイプを介してアクセスされますが、フィールドは特定のインスタンスに格納されます。



foo



と呼ぶBar



新しいインスタンスを作成するとどうなるかを詳しく見てみましょう。



 const foo = new Bar(true);
      
      





このコードを使用して作成されたインスタンスには、単一の'x'



プロパティを持つフォームがあります。 プロトタイプfoo



は、クラスBar



属するBar.prototype



です。







このBar.prototype



はそれ自体の形式を持ち、プロパティ'getX'



のみを含み、その値は関数'getX'



によって決定され、呼び出されるとthis.x



返しますthis.x



プロトタイプBar.prototype



は、JavaScript言語の一部であるObject.prototype



です。 Object.prototype



はプロトタイプツリーのルートですが、そのプロトタイプはnull



です。







同じクラスの新しいインスタンスを作成すると、すでに理解したように、両方のインスタンスのフォームは同じになります。 両方のインスタンスは、同じBar.prototype



オブジェクトをBar.prototype



ます。



プロトタイプのプロパティにアクセスする



さて、クラスを定義して新しいインスタンスを作成するとどうなるかがわかりました。 しかし、次の例のように、インスタンスでメソッドを呼び出すとどうなりますか?



 class Bar { constructor(x) { this.x = x; } getX() { return this.x; } } const foo = new Bar(true); const x = foo.getX(); // ^^^^^^^^^^
      
      





メソッド呼び出しは、2つの個別のステップとして考えることができます。



 const x = foo.getX(); // is actually two steps: const $getX = foo.getX; const x = $getX.call(foo);
      
      





最初のステップはメソッドをロードすることです。これは実際にはプロトタイプのプロパティです(値は関数です)。 2番目のステップは、たとえばthis



の値などのインスタンスを使用して関数を呼び出すthis



です。 foo



インスタンスからgetX



メソッドがgetX



れる最初のステップを詳しく見てみましょう。







エンジンはfoo



インスタンスを開始し、フォームfoo



に'getX'



がないことを認識するため、プロトタイプチェーンを介してそれを見つける必要があります。 Bar.prototype



し、プロトタイプフォームを見て、ゼロオフセットに'getX'



プロパティがあることを確認します。 Bar.prototype



でこのオフセットの値を探し、探していたJSFunction getX



を見つけます。



JavaScriptの柔軟性により、プロトタイプチェーンリンクを変更できます。次に例を示します。



 const foo = new Bar(true); foo.getX(); // → true Object.setPrototypeOf(foo, null); foo.getX(); // → Uncaught TypeError: foo.getX is not a function
      
      





この例では、
 foo.getX()
      
      



2回ですが、毎回完全に異なる意味と結果を持っています。 プロトタイプはJavaScriptの単なるオブジェクトであるという事実にもかかわらず、プロトタイプのプロパティへのアクセスを高速化することは、通常のオブジェクトのプロパティへの独自のアクセスを高速化するよりもJavaScriptエンジンにとってさらに重要なタスクです。



日常のプラクティスでは、プロトタイププロパティの読み込みはかなり一般的な操作です。これはメソッドを呼び出すたびに発生します。



 class Bar { constructor(x) { this.x = x; } getX() { return this.x; } } const foo = new Bar(true); const x = foo.getX(); // ^^^^^^^^^^
      
      





前に、フォームとインラインキャッシュを使用して、エンジンが通常のプロパティの読み込みを最適化する方法について説明しました。 同じ形状のオブジェクトのプロトタイププロパティの読み込みを最適化するにはどうすればよいですか? 上記では、プロパティがどのようにロードされるかを見ました。







この特定のケースで繰り返しダウンロードしてこれを迅速に行うには、次の3つのことを知る必要があります。





一般的な場合、これは、インスタンス自体の1つのチェックと、目的のプロパティを含むプロトタイプまでの各プロトタイプに対してさらに2つのチェックを行う必要があることを意味します。 1 + 2Nチェック(Nは使用されたプロトタイプの数)は、プロトタイプチェーンが比較的浅いため、この場合はそれほど悪くはありません。 ただし、通常のDOMクラスの場合のように、エンジンは多くの場合、より長いプロトタイプチェーンを処理する必要があります。 例:



 const anchor = document.createElement('a'); // → HTMLAnchorElement const title = anchor.getAttribute('title');
      
      





HTMLAnchorElement



あり、 getAttribute()



メソッドを呼び出します。 この単純な要素のチェーンには、すでに6つのプロトタイプが含まれています! 興味のあるDOMメソッドのほとんどは、 HTMLAnchorElement



プロトタイプHTMLAnchorElement



ではなく、チェーンのどこかにあります。







getAttribute()



メソッドはElement.prototype



ます。 つまり、 anchor.getAttribute()



を呼び出すanchor.getAttribute()



に、JavaScriptエンジンは次を必要とします。



  1. 'getAttribute'



    それ自体anchor



    オブジェクトで'getAttribute'



    ないことを確認してください。
  2. 最終プロトタイプがHTMLAnchorElement.prototype



    あることを確認します。
  3. そこに'getAttribute'



    がないことを確認します。
  4. 次のプロトタイプがHTMLElement.prototype



    あることを確認しHTMLElement.prototype



    。
  5. 'getAttribute'



    が存在しないことを'getAttribute'



    確認'getAttribute'



    ます。
  6. 次のプロトタイプがElement.prototype



    あることを確認します。
  7. 'getAttribute'



    れていることを確認してください。


合計7回のチェック。 このタイプのコードはWeb上で非常に一般的であるため、エンジンはさまざまなトリックを使用して、プロトタイププロパティの読み込みに必要なチェックの数を減らします。



foo



'getX'



を要求するときに3つのチェックのみを行った以前の例に戻ります。



 class Bar { constructor(x) { this.x = x; } getX() { return this.x; } } const foo = new Bar(true); const $getX = foo.getX;
      
      





目的のプロパティを含むプロトタイプの前に発生する各オブジェクトについて、このプロパティが存在しないことをフォームで確認する必要があります。 プロトタイプチェックをプロパティの不在のチェックとして提示することにより、チェックの数を減らすことができればいいと思います。 本質的に、これはまさにエンジンが単純なトリックで行うことです。インスタンス自体へのプロトタイプリンクを保存する代わりに、エンジンはそれをフォームに保存します。







各フォームはプロトタイプを示しています。 これは、プロトタイプfoo



が変更されるたびに、エンジンが新しいフォームに移動することを意味します。 次に、特定のプロパティが存在しないことを確認するためにオブジェクトの形状のみをチェックし、プロトタイプリンクを保護する(プロトタイプリンクを保護する)必要があります。



このアプローチにより、必要なチェックの数を2N + 1から1 + Nに減らして、アクセスを高速化できます。 チェーン内のプロトタイプの数の線形関数であるため、これは依然としてかなり高価な操作です。 エンジンはさまざまなトリックを使用して、特に同じプロパティの順次読み込みの場合に、チェックの数を特定の一定値にさらに削減します。



有効性セル



V8は、この目的専用のプロトタイプフォームを処理します。 各プロトタイプには、他のオブジェクト(特に、他のプロトタイプ)と共有されない一意のフォームがあり、これらの各プロトタイプフォームには、それに関連付けられた特別なValidityCell



があります。







このValidityCell



、関連するプロトタイプまたはその上の他のプロトタイプを誰かが変更するたびに無効になります。 仕組みを見てみましょう。

後続のプロトタイプのダウンロードを高速化するために、V8はインラインキャッシュを4フィールドの場所に配置します。







コードが最初に実行されるときにインラインキャッシュが加熱されると、V8は、プロトタイプ、このプロトタイプ( Bar.prototype



)、インスタンスフォーム(この場合はfoo



)でプロパティが見つかったオフセットを記憶し、現在のValidityCell



を受け取ったプロトタイプにバインドしますフォームのインスタンスから(この場合、 Bar.prototype



が取得されます)。



次回インラインキャッシュを使用するとき、エンジンはインスタンスフォームとValidityCell



を確認する必要があります。 それでも有効な場合、エンジンはプロトタイプのオフセットを直接使用し、追加の検索ステップをスキップします。







プロトタイプが変更されると、新しいフォームが強調表示され、以前のValidityCell



セルが無効になります。 このため、インラインキャッシュは次回の起動時にスキップされ、パフォーマンスが低下します。



DOM要素を使用した例に戻りましょう。 Object.prototype



各変更は、 Object.prototype



のインラインキャッシュを無効にするだけでなく、 EventTarget.prototype



、 Node.prototype



、 Element.prototype



など、 HTMLAnchorElement.prototype



自体までを含む、その下のチェーン内のプロトタイプのインラインキャッシュも無効にします。







実際、コードの実行中にObject.prototype



を変更すると、パフォーマンスがObject.prototype



低下します。 これをしないでください!



これがどのように機能するかをよりよく理解するために、特定の例を見てみましょう。 Bar



クラスと、 Bar



型のオブジェクトのメソッドを呼び出すloadX



関数があるとします。 同じクラスのインスタンスでloadX



関数を数回呼び出します。



 class Bar { /* … */ } function loadX(bar) { return bar.getX(); // IC for 'getX' on `Bar` instances. } loadX(new Bar(true)); loadX(new Bar(false)); // IC in `loadX` now links the `ValidityCell` for // `Bar.prototype`. Object.prototype.newMethod = y => y; // The `ValidityCell` in the `loadX` IC is invalid // now, because `Object.prototype` changed.
      
      





loadX



のインラインキャッシュは、 loadX



ValidityCell



を指すようにBar.prototype



。 次に、JavaScriptのすべてのプロトタイプのルートである( Object.prototype



) Object.prototype



変更すると、 ValidityCell



が無効になり、既存のインラインキャッシュが次回使用されなくなり、パフォーマンスが低下します。



Object.prototype



変更すると、変更時にロードされたプロトタイプのインラインキャッシュが無効になるため、常に悪い考えです。 禁止事項の例を次に示します。



 Object.prototype.foo = function() { /* … */ }; // Run critical code: someObject.foo(); // End of critical code. delete Object.prototype.foo;
      
      





この時点でエンジンによってロードされたすべてのインラインプロトタイプキャッシュを無効にするObject.prototype



を拡張します。 次に、説明したメソッドを使用するコードを実行します。 エンジンは最初から開始し、プロトタイププロパティへのアクセスのためにインラインキャッシュを構成する必要があります。 そして最後に、以前に追加したプロトタイプメソッドを「クリーンアップ」して削除します。



掃除は良いアイデアだと思いますか? さて、この場合、状況はさらに悪化します! プロパティを削除するとObject.prototype



が変更さObject.prototype



ため、すべてのインラインキャッシュが再び無効になり、エンジンは最初から作業を再開する必要があります。



まとめると 。 プロトタイプは単なるオブジェクトであるという事実にもかかわらず、プロトタイプによるメソッド検索のパフォーマンスを最適化するために、JavaScriptエンジンによって特別に処理されます。 プロトタイプはそのままにしてください! または、本当にそれらを処理する必要がある場合は、コードを実行する前に実行してください。そうすれば、実行中にコードを最適化するすべての試みを少なくとも無効にしないでください。



要約する



JavaScriptがオブジェクトとクラスを保存する方法、およびフォーム、インラインキャッシュ、有効性セルがプロトタイプ操作を最適化する方法を学びました。 この知識に基づいて、実用的な観点からパフォーマンスを改善する方法を理解しました。プロトタイプに手を触れないでください! (または本当に必要な場合は、コードを実行する前に実行してください)。


← 前半



この一連の出版物は役に立ちましたか? コメントを書いてください。



All Articles