ES2015でObject#toStringのパフォーマンスを調査して最適化する

GoogleのミュンヘンオフィスのBenedict Meyrerは、JavaScriptの最適化に取り組んでいます。 この記事では、V8エンジンでのObject.prototype.toString()



実装および機能の機能についてObject.prototype.toString()



しています。 特に、この設計が重要である理由、ES2015キャラクターの出現によってどのように変化したか、V8でtoString()



パフォーマンスが約6倍向上したMozillaエンジニアが提案した最適化アプローチについて説明します。



画像



はじめに



ECMAScript 2015では、いわゆる既知のシンボルの概念が導入されました。 これらは、言語の内部メカニズムを表す特別な組み込み文字であり、ECMAScript 5および以前のバージョンの標準の実装では開発者がアクセスできません。



以下に例を示します。





これらの新しい構成のほとんどは、言語のさまざまな部分で包括的かつ非自明に機能します。 これは、いわゆるモンキーパッチによるパフォーマンスプロファイルの大幅な変更につながります 。 プログラムの実行中にユーザーコードを使用していくつかの標準メカニズムを変更する可能性について話している。



特に興味深い例の1つは、新しいSymbol.toStringTagシンボルです。これは、組み込みのObject.prototype.toString()メソッドの動作を制御するために使用されます。 たとえば、開発者はオブジェクトの任意のインスタンスに特別なプロパティを配置できます。その後、 toString



メソッドを呼び出すときに、標準のインラインタグの代わりにこのプロパティが使用されます。



 class A { get [Symbol.toStringTag]() { return 'A'; } } Object.prototype.toString.call('');     // "[object String]" Object.prototype.toString.call({});     // "[object Object]" Object.prototype.toString.call(new A);  // "[object A]"
      
      





このため、ES2015以降のバージョンの標準のObject.prototype.toString()



実装では、最初に抽象ToObject操作を使用してこのをオブジェクトに変換し、次に、結果のオブジェクトとそのプロトタイプチェーンでSymbol.toStringTag



を検索するSymbol.toStringTag



があります。 言語仕様の対応する部分でそれについて見つけることができます:





Object.prototype.toString()の仕様スニペット



ここでは、最初にToObjectを使用した変換、次に@@toStringTag



Get呼び出しを見ることができます(これは、 toStringTag



という名前の有名な文字の言語仕様の特別な内部構文です)。 Symbol.toStringTag



コンストラクトをES2015に追加すると、開発者の機能が大幅に拡張されますが、同時に一定量のリソースも必要になります。



ToStringパフォーマンス研究の目標



ChromeおよびNode.jsのObject.prototype.toString()



メソッドのパフォーマンスは、いくつかの一般的なフレームワークおよびライブラリによる型チェックに頻繁に使用されるため、 すでに調査されています。 そのため、AngularJSフレームワークはこのメソッドを使用して、 angle.isDateangle.isArrayBufferangle.isRegExpなどのさまざまなヘルパー関数を実装します。 例:



 /** * @ngdoc function * @name angular.isDate * @module ng * @kind function * * @description * Determines if a value is a date. * * @param {*} value Reference to check. * @returns {boolean} True if `value` is a `Date`. */ function isDate(value) { return toString.call(value) === '[object Date]'; }
      
      





さらに、 lodashunderscore.jsなどの一般的なライブラリは、 Object.prototype.toString()



を使用して値チェックを実装します。 そのため、たとえば、 lodashの述語_.isPlainObjectおよび_.isDateが配置されます。



 /** * Checks if `value` is classified as a `Date` object. * * @since 0.1.0 * @category Lang * @param {*} value The value to check. * @returns {boolean} Returns `true` if `value` is a date object, else `false`. * @example * * isDate(new Date) * // => true * * isDate('Mon April 23 2012') * // => false */ function isDate(value) { return isObjectLike(value) && baseGetTag(value) == '[object Date]' }
      
      





SpiderMonkey JavaScriptエンジンに取り組んでいるMozillaエンジニアは、 Object.prototype.toString()



Symbol.toStringTag



検索Symbol.toStringTag



が実際のアプリケーションのパフォーマンスのボトルネックであることを発見しました。 この結論は、ベンチマーク速度計の研究中になされました。 内部V8プロファイラーを使用して--no-sandbox --js-flags=--prof



からAngularJSテストのみを実行します(有効にするには、コマンドラインキー--no-sandbox --js-flags=--prof



起動時に--no-sandbox --js-flags=--prof



を渡す必要があります)。 @@toStringTag



検索( GetPropertyStub



)の実行と、組み込みObject.prototype.toString()



メソッドを実装するObjectProtoToString



コードの実行に費やされます。





SpeedometerベンチマークからのAngularJSサブテストプロファイリング



SpiderMonkey開発チームのJan de Moischは、配列内のObject.prototype.toString()



パフォーマンスをテストするための簡単なマイクロベンチマークを作成しました。



 function f() {   var res = "";   var a = [1, 2, 3];   var toString = Object.prototype.toString;   var t = new Date;   for (var i = 0; i < 5000000; i++) res = toString.call(a);   print(new Date - t);   return res; } f();
      
      





実際、V8に組み込まれている内部プロファイラーを使用したこのマイクロ--prof



コマンドライン--prof



を使用してd8シェルで有効にすることができます)は、すでに問題の本質を示しています。 主なリソースは、配列[1, 2, 3]



Symbol.toStringTag



検索にSymbol.toStringTag



れます。 合計実行時間の約73%は、結果を与えないプロパティ検索(ユニバーサルプロパティ検索を実装するGetPropertyStub



関数)に費やされ、さらに3%は組み込みToObject



関数にToObject



ます。これは、配列の場合、空の操作(配列、 JavaScriptはすでにオブジェクトです)。





プロファイラーによるMozillaマイクロベンチマークの調査(最適化前)



面白いキャラクター



SpiderMonkeyについては、上記の問題の解決策が提案されました。これは、オブジェクトに興味深いシンボルを追加することで構成されています。 このシンボルは任意の非表示クラスのプロパティであり、この非表示クラスを持つオブジェクトが@@toStringTag



または@@toPrimitive



という名前のプロパティを持つことができるかどうかを示します。 リソースをSymbol.toStringTag



検索のこのアプローチのおかげで、この検索で​​は結果が得られないため、一般に回避できます。 この提案の実装により、SpiderMonkeyのアレイを使用したマイクロベンチマークのパフォーマンスが約2倍向上しました。



AngularJSの使用法をいくつか調べたので、このアイデアを見つけることができて非常に幸運だと感じ、V8で実装することにしました。 ソリューションのアーキテクチャについて考え始め、その結果、V8に移植しましたが、 Symbol.toStringTag



Object.prototype.toString()



限定されました。 実際、 Symbol.toPrimitive



がChromeまたはNode.jsの重要な問題の原因であるという証拠は見つかりませんでした(見つかったまで)。 ここでの主な考え方は、デフォルトでは、オブジェクトインスタンスに興味深い文字がないと想定し、インスタンスに新しいプロパティを追加するたびに、このプロパティの名前が類似した文字であるかどうかを確認することです。 その場合、オブジェクトインスタンスの非表示クラスに特定のビットを設定します。



 const obj = {}; Object.prototype.toString.call(obj);  //     obj[Symbol.toStringTag] = 'a'; Object.prototype.toString.call(obj);  //    
      
      





この簡単な例を見てください。 次に、 興味深いシンボルを持たずに、 obj



オブジェクトが存在し始めます。 したがって、 Object.prototype.toString()



呼び出しは、 Object.prototype.toString()



の検索をスキップできる場合、新しい高速な実行パスにSymbol.toStringTag



(これは、 Object.prototype



興味深いシンボルがないためObject.prototype



)。 2番目の呼び出しは、 obj



興味深い文字があるため、通常の低速検索操作を実行します。



最適化結果



V8でこのメカニズムを実装すると、上記のマイクロベンチマークのパフォーマンスが約5.8倍向上しました。 テストは、Linux、HP Z620ワークステーションで実施されました。 プロファイラーでパフォーマンスを確認すると、プログラムがGetPropertyStub



時間を費やしていないことがわかります。 代わりに、 Object.prototype.toString()



メソッドにより、予想どおりシステムの主な負荷が作成されます。





プロファイラーを使用してMozillaで開発されたマイクロベンチマークの研究(最適化後)



また、 ベンチマークに基づいて最適化されたエンジンをテストしましたが、これは少し現実に近いものです。 パフォーマンスの測定中に、 Object.prototype.toString()



は、特別に設定されたSymbol.toStringTag



プロパティを持つプリミティブやオブジェクトを含むさまざまな値をSymbol.toStringTag



ます。 その結果、V8の最新バージョンはV8 6.1より6.5倍高速でした。





V8のさまざまなバージョンでの新しいマイクロベンチマークの結果



Speedometerブラウザーベンチマーク、特にAngularJSテストでの最適化の影響の測定では、すべてのテストで1%の速度の向上、AngularJSテストを実行すると3%の確実な増加が示されました。





ベンチマーク速度計に対する最適化の影響



まとめ



Object.prototype.toString()



ような高度に最適化された組み込みJavaScript関数でObject.prototype.toString()



、さらに最適化される可能性があります。 特に、上記の最適化により、生産性が最大6.5倍向上します。 SpeedometerベンチマークのAngularJSサブテストなど、さまざまなパフォーマンステストの結果を十分に深く掘り下げると、これを確認できます。



Jan de MoisTom Schusterの研究と興味深いシンボルを使った素晴らしいアイデアに感謝します



WebKitで使用されるJavaScriptエンジンであるJavaScriptCoreは、オブジェクトインスタンスの非表示クラスのObject.prototype.toString()



連続呼び出しの結果をキャッシュすることに注意してください(このキャッシュは、ES2015仕様がリリースされる前の2012年の初めに登場しました)。 これは非常に興味深い戦略ですが、その範囲は限られています(つまり、 Symbol.toPrimitiveSymbol.hasInstanceなどの他の有名なシンボルに適用する場合は役に立ちません )。 さらに、プロトタイプチェーンの変更に対するタイムリーな応答を保証するために、非常に複雑なキャッシュ無効化ロジックが必要です。 そのため、少なくとも現時点では、V8のキャッシュベースのソリューションを支持しない選択をしました。



親愛なる読者! JavaScriptアプリケーションのプロファイルを作成していますか? V8に実装されているどの標準JSメカニズムに最適化が必要ですか?



All Articles