JavaScriptでのオブジェクトの高速複製

クローン JavaScriptでのオブジェクトの複製は、かなり一般的な操作です。 残念ながら、JSはこの問題を解決するための高速なネイティブメソッドを提供していません。

たとえば、プロジェクトのバックエンドで使用している人気のあるNode.JS ORM Sequelizeは、1つだけのクローンで 、多数(1000以上)の行のプリフェッチでパフォーマンスを大幅に失います。 これとともに、たとえばビジネスロジックで、よく知られているlodashライブラリのcloneメソッドを使用すると、パフォーマンスが何十倍も低下します。

しかし、結局のところ、すべてがそれほど悪いわけではなく、たとえばV8 JavaScriptエンジンなどの最新のJSエンジンは、アーキテクチャソリューションが正しく使用されていれば、このタスクにうまく対処できます。 30ミリ秒で100万個のオブジェクトを複製する方法を知りたい場合は、猫にようこそ、他の人はすぐに実装を見ることができます

すぐにこのトピックについて少し書かれていることを予約したいと思います。 Habrの同僚は、ネイティブ拡張node-v8-cloneを作成しましたが、ノードの新しいバージョン用にアセンブルされず、その範囲はバックエンドによってのみ制限され、速度は提案されたソリューションよりも遅くなります。

クローン作成中に費やされるプロセッサ時間を把握しましょう。これらは、メモリ割り当てと記録という2つの主要な操作です。 一般に、それらの実装は多くのJSエンジンで似ていますが、次にNode.jsの主要なものとしてV8について説明します。 まず、時間がかかっていることを理解するには、JavaScriptオブジェクトが何であるかを理解する必要があります。

JavaScriptオブジェクトの表現


JSは非常に柔軟なプログラミング言語であり、そのオブジェクトのプロパティをオンザフライで追加できます。ほとんどのJSエンジンはハッシュテーブルを使用してそれらを表現します。これにより、必要な柔軟性が得られますが、プロパティへのアクセスが遅くなります。 辞書での動的ハッシュ検索が必要です。 したがって、V8最適化コンパイラーは、速度を追求して、辞書(ハッシュテーブル)と隠しクラス(高速、オブジェクト内プロパティ)の2種類のオブジェクト表現をオンザフライで切り替えることができます。

可能な限り、V8は隠されたクラスを使用してオブジェクトプロパティにすばやくアクセスしようとしますが、ハッシュテーブルは「複雑な」オブジェクトを表すために使用されます。 V8の非表示クラスは、オブジェクトのプロパティ記述子、そのサイズ、およびコンストラクターとプロトタイプへの参照のテーブルを含むメモリ内の構造にすぎません。 例として、JSオブジェクトの古典的な表現を考えてみましょう。
 function Point(x, y) { this.x = x; this.y = y; } 

new Point(x, y)を実行すると、新しいPointオブジェクトが作成されます。 V8がこれを初めて実行するとき、 Pointベース隠しクラスを作成します。例としてC0と呼びましょう。 なぜなら オブジェクトのプロパティがまだ定義されていないため、非表示のクラスC0空です。
C0

Point最初の式を実行すると( this.x = x; )、 Pointオブジェクトに新しいプロパティxが作成されます。 同時に、V8:


C1

Point 2番目の式( this.y = y; )を実行すると、 Pointオブジェクトに新しいyプロパティが作成されます。 同時に、V8:


C2

新しいプロパティが追加されるたびに非表示のクラスを作成することは効果的ではありませんが、 同じオブジェクトの新しいインスタンスの場合、隠しクラスは再利用されます-V8は辞書の代わりにそれらを使用するよう努めています。 隠されたクラスのメカニズムは、プロパティにアクセスするときに辞書検索を回避するのに役立ち、また、以下を含むさまざまなクラスベースの最適化を使用できます。 インラインキャッシング

この点で、コンパイラの理想的なオブジェクトはオブジェクトになります-一連のプロパティが明確に定義され、実行中に変化しないコンストラクタを備えたオブジェクトです。 したがって、複製中にオブジェクトのプロパティへのアクセスを高速化するための最も重要な最適化は、コンストラクターの正しい説明です。 2番目の重要な部分は、読み取り/書き込みプロセス自体の直接的な最適化です。これについては、後で説明します。

動的コード生成


V8は、最初の実行時にJavaScriptコードをマシンに直接コンパイルします。中間コードやインタープリターは必要ありません。 オブジェクトのプロパティへのアクセスは最適化されたインラインキャッシュであり、V8のマシン命令は実行時に直接変更できます。

コードの最初の実行中にオブジェクトのプロパティを読み取ることを検討してください。V8は現在の非表示クラスを決定し、今後の呼び出しを最適化し、コードのこのセクションで同じ非表示クラスを持つオブジェクトを予測します。 V8が正しく予測された場合、プロパティの値は1回の操作で割り当てられます(または取得されます)。 正しく予測できなかった場合、V8はコードを変更し、最適化を削除します。

たとえば、 Pointオブジェクトのxプロパティを受け取るJavaScriptコードを使用します。
 point.x 

V8は、 xを読み取るために次のマシンコードを生成します。
 # ebx = the point object cmp [ebx,<hidden class offset>],<cached hidden class> jne <inline cache miss> mov eax,[ebx, <cached x offset>] 

オブジェクトの隠しクラスがキャッシュされたクラスと一致しない場合、実行はV8コードに進み、インラインキャッシュの不在を処理して変更します。 ほとんどの場合に発生するクラスが一致する場合、 xプロパティの値は1回の操作で取得されます。

同じ隠しクラスで複数のオブジェクトを処理する場合、ほとんどの静的言語と同じ利点が得られます。 非表示クラスを使用してオブジェクトプロパティにアクセスし、キャッシュを使用することにより、JavaScriptコードのパフォーマンスが大幅に向上します。 クローン作成プロセスを高速化するために使用するのは、これらの最適化です。

クローニング


上記の理論からわかったように、2つの条件が満たされた場合、クローン作成は最速になります。

つまり、 Pointオブジェクトのクローンをすばやく作成するには、このタイプのオブジェクトを受け取り、それに基づいて新しいオブジェクトを作成するコンストラクターを作成する必要があります。
 function Clone(point) { this.x = point.x; this.y = point.y; } var clonedPoint = new Clone(point); 

原則として、それがすべてであり、1つのことがなければ、システム内のすべての種類のオブジェクトにこのようなコンストラクターを記述するのは非常に高価であり、オブジェクトは複雑なネスト構造を持つこともできます。 これらの最適化を使用して作業を簡素化するために、ネストされたオブジェクトの転送用のクローン作成コンストラクターを作成するライブラリーを作成しました。

ライブラリの動作原理は非常に単純です。オブジェクトを入力として受け取り、その構造からクローン作成コンストラクターを生成します。このコンストラクターは、後でこのタイプのオブジェクトのクローン作成に使用できます。
 var Clone = FastClone.factory(point); var clonedPoint = new Clone(point); 

この関数はevalを介して生成され、操作は安価ではないため、主に同じ構造のオブジェクトを再クローンする必要がある場合にパフォーマンス上の利点が得られます。 bench.jsを使用したChromium 50.0.2661.102 Ubuntu 14.04(64ビット)ブラウザーのベンチマーク結果:
図書館操作/秒
ファストクローン16 927 673
Object.assign535 911
ロダシュ66 313
JQuery62 164
テストソース-jsfiddle.net/volovikov/thcu7tjv/25
一般に、実際のシステムでも同じ結果が得られます。複製は、繰り返し構造を持つオブジェクトで100〜200倍加速されます。

ご清聴ありがとうございました!

ライブラリ-github.com/ivolovikov/fastest-clone

関連資料:
jayconrod.com/posts/52/a-tour-of-v8-object-representation
developers.google.com/v8/design

Source: https://habr.com/ru/post/J283090/


All Articles