web-dev-qa-db-ja.com

不変性はJavaScriptのパフォーマンスを低下させますか?

JavaScriptには、データ構造を不変として扱う傾向があるという最近の傾向があるようです。たとえば、オブジェクトの単一のプロパティを変更する必要がある場合は、新しいプロパティで新しいオブジェクト全体を作成し、古いオブジェクトから他のすべてのプロパティをコピーして、古いオブジェクトをガベージコレクションすることをお勧めします。 (それはとにかく私の理解です。)

私の最初の反応は、それはパフォーマンスに悪いように聞こえるということです。

しかし Immutable.jsRedux.js のようなライブラリは私よりも賢い人によって書かれており、パフォーマンスに強い関心を持っているように見えるので、ガーベッジ(およびそのパフォーマンスへの影響)の理解は間違っています。

私が見逃している不変性にパフォーマンス上の利点はありますか、そしてそれらは多くのゴミを作成することの欠点を上回っていますか?

94
callum

たとえば、オブジェクトの単一のプロパティを変更する必要がある場合は、新しいプロパティで新しいオブジェクト全体を作成し、古いオブジェクトから他のすべてのプロパティをコピーして、古いオブジェクトをガベージコレクションすることをお勧めします。

不変性がないと、異なるスコープ間でオブジェクトを渡す必要があり、オブジェクトが変更されるかどうか、いつ変更されるかは事前にわかりません。したがって、不要な副作用を回避するために、オブジェクトの完全なコピーの作成を開始し、プロパティをまったく変更する必要がない場合でも、そのコピーを渡します。それはあなたの場合よりもはるかに多くのゴミを残します。

これが示すことは、正しい仮説シナリオを作成すれば、特にパフォーマンスに関しては、何でも証明できるということです。しかし、私の例は、聞こえるかもしれないほど仮説ではありません。私は先月、不変のデータ構造を使用することを最初に決めたのでその問題に正確に出くわしたプログラムに取り組みましたが、手間がかかるように思われなかったため、後でこれをリファクタリングすることをためらっていました。

これは古いSO post のようなケースを見ると、質問への答えはおそらく明確になります-it depends場合によっては、不変性がパフォーマンスを損なう場合があり、その逆の場合もあります。多くの場合、実装のスマートさに依存します。さらに多くの場合、違いは無視できる。

最後の注意:発生する可能性のある現実の問題は、一部の基本的なデータ構造の不変性を早期に決定するか否かを決定する必要があることです。次に、その上に多数のコードを作成します。数週間後または数か月後に、決定が良かったか悪かったかがわかります。

この状況における私の個人的な経験則は次のとおりです。

  • プリミティブ型またはその他の不変タイプに基づいて少数の属性のみでデータ構造を設計する場合は、最初に不変性を試してください。
  • サイズが大きい(または未定義の)配列、ランダムアクセス、および内容の変更が含まれるデータ型を設計する場合は、可変性を使用します。

これら2つの極端な状況の間にある場合は、判断を下してください。しかしYMMV。

60
Doc Brown

まず第一に、不変のデータ構造の特徴付けは不正確です。一般に、データ構造のほとんどはコピーされませんが、shared、変更された部分のみがコピーされます。 永続的なデータ構造 と呼ばれます。ほとんどの実装では、ほとんどの場合、永続的なデータ構造を利用できます。パフォーマンスは、関数型プログラマーが一般に無視できると見なす可変データ構造に十分近いです。

第二に、多くの人が、典型的な命令型プログラムのオブジェクトの典型的な寿命についてかなり不正確な考えを持っていることを発見しました。おそらく、これはメモリ管理言語の人気によるものです。しばらく座って、実際に長期間有効なデータ構造と比較して、作成した一時オブジェクトと防御コピーの数を実際に見てください。その比率には驚かれることでしょう。

私は関数型プログラミングのクラスで、アルゴリズムが生成するガベージの量について教えてくれた人がいました。次に、同じ量のアルゴリズムを作成する、同じアルゴリズムの典型的な命令バージョンを示します。なんらかの理由で、もう気付かない人がいます。

変数を入力する有効な値が得られるまで変数の共有を奨励し、変数の作成を奨励​​することにより、不変性は、よりクリーンなコーディング手法とより長い寿命のデータ構造を促進する傾向があります。これは、アルゴリズムによっては、低レベルではないにしても同等レベルのガベージレベルにつながることがよくあります。

39
Karl Bielefeldt

このQ&Aに遅れをとって、すでにすばらしい回答を得ていますが、メモリ内のビットとバイトの下位レベルの観点から物事を見ることに慣れている外国人として侵入したいと思いました。

不変のデザインに非常に興奮しています。Cの観点からも、最近のこのようなハードウェアを効果的にプログラムするための新しい方法を見つける観点からもです。

遅い/速い

それが物事を遅くするかどうかの質問に関しては、ロボットの答えはyesでしょう。この種の非常に技術的な概念レベルでは、不変性は物事を遅くするだけです。ハードウェアは、メモリを散発的に割り当てているのではなく、既存のメモリを変更できる場合に最適です(時間的局所性のような概念があるためです)。

それでも実際的な答えはmaybeです。パフォーマンスは依然として、重要なコードベースでは、主に生産性の指標です。たとえバグを無視したとしても、通常、競合状態を越えて維持するという恐ろしいコードベースが最も効率的であるとは言えません。多くの場合、効率は優雅さとシンプルさの関数です。マイクロ最適化のピークは多少矛盾する可能性がありますが、それらは通常、コードの最小かつ最も重要なセクションのために予約されています。

不変のビットとバイトの変換

低レベルの観点から見ると、objectsstringsなどのX線の概念などの場合、その中心はさまざまな速度のさまざまな形式のメモリ内のビットとバイトだけです。/size特性(通常、メモリハードウェアの速度とサイズは相互に排他的です)。

enter image description here

上記の図のように、同じメモリチャンクに繰り返しアクセスすると、コンピュータのメモリ階層が好まれます。これは、頻繁にアクセスされるメモリチャンクを最高速のメモリ(L1キャッシュ、たとえば、レジスタとほぼ同じ速度です)。まったく同じメモリに繰り返しアクセスする(複数回再利用する)か、チャンクの異なるセクションに繰り返しアクセスする場合があります(たとえば、メモリのチャンクのさまざまなセクションに繰り返しアクセスする連続したチャンクの要素をループする)。

次のように、このメモリを変更すると、まったく新しいメモリブロックをサイドに作成したい場合、そのプロセスでレンチを投げることになります。

enter image description here

...この場合、新しいメモリブロックにアクセスするには、それを強制的にページフォールトとキャッシュミスにして、最速のメモリ(レジスタに至るまで)に戻す必要があります。それは本当のパフォーマンスキラーになることができます。

これを軽減する方法はいくつかありますが、すでに触れられている、事前に割り当てられたメモリの予約プールを使用します。

大きな集計

少し高いレベルのビューから発生するもう1つの概念的な問題は、非常に大きな集合体の不要なコピーを一括で行うことです。

過度に複雑な図を回避するために、この単純なメモリブロックが何らかの形で高価であったとしましょう(信じられないほど限られたハードウェアでは、UTF-32文字の可能性があります)。

enter image description here

この場合、 "HELP"を "KILL"に置き換えて、このメモリブロックが不変である場合、一部だけが変更されていても、一意の新しいオブジェクトを作成するには、全体を新しいブロックとして作成する必要があります。 :

enter image description here

私たちの想像力をかなり引き伸ばすと、ほんの一部を一意にするために他のすべてのものをこのようにディープコピーすると、かなり高価になる可能性があります(実際のケースでは、このメモリブロックは、問題を引き起こすためにはるかに大きくなります)。

ただし、このような費用にもかかわらず、この種の設計は人的ミスが発生しにくい傾向があります。純粋な関数を使用した関数型言語で作業したことがある人なら誰でも、おそらくこれを高く評価できます。特に、このようなコードを世間で気にすることなくマルチスレッド化できるマルチスレッドの場合はそうです。一般に、人間のプログラマは状態の変化、特に現在の関数のスコープ外の状態に外部の副作用を引き起こすものをつまずきがちです。このような場合、外部エラー(例外)からの回復でさえ、ミックス内の外部状態の変化が変化すると非常に困難になる可能性があります。

この冗長なコピー作業を軽減する1つの方法は、次のように、これらのメモリブロックを文字へのポインタ(または参照)のコレクションにすることです。

お詫び、図を作成するときにLを一意にする必要がないことに気付きませんでした。

青は浅いコピーされたデータを示します。

enter image description here

...残念ながら、これは1文字あたりのポインタ/参照コストを支払うのに信じられないほど高くつくでしょう。さらに、文字の内容をアドレススペース全体に分散させ、ページフォールトとキャッシュミスのボートロードの形で料金を支払うことになり、全体を完全にコピーするよりも簡単にこのソリューションがさらに悪化する可能性があります。

これらの文字を連続して割り当てるように注意していたとしても、マシンが8文字と文字への8ポインタをキャッシュラインにロードできるとしましょう。新しい文字列をたどるには、次のようにメモリをロードします。

enter image description here

この場合、理想的には3つだけ必要な場合、この文字列をトラバースするには、7つの異なるキャッシュラインに相当する連続メモリをロードする必要があります。

データをチャンクアップ

上記の問題を軽減するために、同じ基本的な戦略を適用することができますが、8文字のより粗いレベルで使用できます。

enter image description here

結果は、この文字列をトラバースするために、理論上の最適値の1つだけ不足している4キャッシュライン相当のデータ(1つは3ポインタ、3つは文字)をロードする必要があります。

ですから、それはまったく悪くありません。多少のメモリの浪費はありますが、メモリは十分にあり、追加のメモリが頻繁にアクセスされないコールドデータになるだけの場合でも、メモリを使い果たしても速度が低下することはありません。これは、メモリの使用量と速度の低下が密接に関係しているホットで隣接するデータにのみ当てはまり、より多くのメモリを単一のページまたはキャッシュラインに収め、エビクションの前にすべてにアクセスする必要があります。この表現はかなりキャッシュフレンドリーです。

速度

したがって、上記のような表現を利用すると、パフォーマンスのバランスがかなり良くなります。おそらく、不変データ構造の最もパフォーマンスが重要な使用法は、データのチャンク部分を変更し、変更されていない部分を浅くコピーしながら、プロセスでそれらを一意にするというこの性質を引き受けます。また、マルチスレッドコンテキストで浅いコピーされた部分を安全に参照するために、アトミック操作のオーバーヘッドが含まれていることも意味します(アトミック参照カウントが行われている可能性があります)。

しかし、これらの分厚いデータが十分に粗いレベルで表されている限り、このオーバーヘッドの多くは減少し、場合によっては単純化されますが、外部側のない純粋な形式でより多くの関数のコーディングとマルチスレッド化の安全性と容易さを提供します効果。

新しいデータと古いデータの保持

パフォーマンスの観点から(実用的な意味で)不変性が最も役立つ可能性があると私が考えるのは、大きなデータのコピー全体を作成して、新しいデータを生成することを目的とする可変コンテキストでそれを一意にしたくなる場合です。注意深く不変のデザインを使って、ほんの少しだけ個性的なものを作ることができたときに、新しいものと古いものの両方を維持したい方法ですでに存在しているもの。

例:システムを元に戻す

この例は、元に戻すシステムです。データ構造の小さな部分を変更し、元に戻すことができる元のフォームと新しいフォームの両方を保持したい場合があります。データ構造の小さな変更されたセクションのみを一意にするこの種の不変の設計により、追加された一意の部分のデータのメモリコストを支払うだけで、元のデータのコピーを元に戻すエントリに格納できます。これにより、生産性(元に戻すシステムの実装を簡単にする)とパフォーマンスの非常に効果的なバランスが得られます。

高レベルのインターフェース

しかし、上記のケースでは厄介なことが発生します。ローカルな種類の関数コンテキストでは、変更可能なデータが変更が最も簡単で簡単です。結局のところ、配列を変更する最も簡単な方法は、配列をループして一度に1つの要素を変更することです。配列を変換するために多数の高レベルのアルゴリズムから選択し、適切なアルゴリズムを選択して、変更されたパーツが変更されている間にこれらのチャンクの浅いコピーが確実に作成されるようにした場合、知的オーバーヘッドが増加する可能性があります。ユニークにしました。

おそらくこれらのケースで最も簡単な方法は、変更可能なデータバッファーにアトミックに変更をコミットして新しい不変のコピーを取得する関数のコンテキスト内でローカルで可変バッファーを使用することです(通常、一部の言語では、これらの「一時的」)...

...または、データに対して上位および上位レベルの変換関数を単純にモデル化し、可変ロジックを使用せずに可変バッファを変更して構造にコミットするプロセスを非表示にすることができます。いずれにせよ、これはまだ広く探究されていない領域であり、これらのデータ構造を変換する方法に意味のあるインターフェースを考え出すために不変の設計をさらに採用する場合、私たちは作業を切り捨てています。

データ構造

ここで発生するもう1つのことは、パフォーマンスが重要なコンテキストで使用される不変性により、データ構造がチャンクのサイズが小さすぎず大きすぎないチャンキーデータに分解される可能性があるということです。

リンクされたリストは、これに対応するためにかなりの変更を加えて、展開されたリストに変える必要がある場合があります。大きな連続した配列は、ランダムアクセス用のモジュロインデックスを使用して、ポインターの配列から連続したチャンクに変わる可能性があります。

興味深い方法でデータ構造の見方を変える可能性がありますが、これらのデータ構造の変更機能をかさばる性質に押し上げて、ここでいくつかのビットを浅くコピーして他のビットを一意にすることによる余分な複雑さを隠します。

パフォーマンス

とにかく、これは私のトピックに関する少し下位のビューです。理論的には、不変性には非常に大きなものから小さなものまでさまざまなコストがかかります。しかし、非常に理論的なアプローチでは、常にアプリケーションが高速になるわけではありません。それはそれらをスケーラブルにするかもしれませんが、実際の速度はより実用的な考え方を受け入れることをしばしば必要とします。

実用的な観点から見ると、パフォーマンス、保守性、安全性などの品質は、特に非常に大規模なコードベースの場合、1つの大きな不鮮明になる傾向があります。絶対的な意味でのパフォーマンスは不変性によって低下しますが、それが生産性と安全性(スレッドセーフを含む)にもたらす利点を主張することは困難です。これらが増えると、開発者がバグに煩わされることなくコードを調整および最適化するためのより多くの時間を持つためでさえ、実際のパフォーマンスが向上することがよくあります。

だから私はこの実用的な意味から、不変のデータ構造は実際にはaidパフォーマンスを多くの場合、奇妙に聞こえるかもしれません。理想的な世界では、これら2つの不変データ構造と可変データ構造の混合が求められる可能性があります。可変データ構造は通常、非常にローカルなスコープ(例:関数のローカル)で非常に安全に使用できますが、不変データ構造は外部側を回避できます。完全に影響し、データ構造へのすべての変更をアトミック操作に変換して、競合状態のリスクのない新しいバージョンを生成します。

34
user204677

ImmutableJSは実際には非常に効率的です。例をとると:

var x = {
    Foo: 1,
    Bar: { Baz: 2 }
    Qux: { AnotherVal: 3 }
}

上記のオブジェクトを不変にすると、 'Baz'プロパティの値を変更して、次のようになります。

var y = x.setIn('/Bar/Baz', 3);
y !== x; // Different object instance
y.Bar !== x.Bar // As the Baz property was changed, the Bar object is a diff instance
y.Qux === y.Qux // Qux is the same object instance

これにより、ルートへのパス上のオブジェクトの値型をコピーするだけでよい、ディープオブジェクトモデルのパフォーマンスが大幅に向上します。オブジェクトモデルが大きく、変更が小さいほど、多くのオブジェクトを共有するので、不変のデータ構造のメモリとCPUのパフォーマンスが向上します。

他の答えが言ったように、これを、それを操作できる関数に渡す前にxを防御的にコピーすることによって同じ保証を提供しようとすることと対比すると、パフォーマンスが大幅に向上します。

11
Bringer128

この(すでにうまく答えられた)質問に追加するには:

短い答えはyes;です。既存のオブジェクトを変更するのではなくオブジェクトを作成するだけなので、パフォーマンスが低下し、オブジェクト作成のオーバーヘッドが増加します。


しかし、長い答えはそれほどではないです。

実際のランタイムの観点から見ると、JavaScriptでは既にかなりの数のランタイムオブジェクトを作成しています。関数やオブジェクトリテラルはJavaScriptのいたるところにあり、誰もそれらの使用について二度と考えていないようです。オブジェクトの作成は実際には非常に安価であると主張しますが、これについての引用はないので、スタンドアロンの引数としては使用しません。

私にとって、最大の「パフォーマンス」の向上は、ランタイムパフォーマンスではなく、developerパフォーマンスにあります。 Real World(tm)アプリケーションの作業中に最初に学んだことの1つは、可変性は本当に危険で混乱を招くということです。それがいまいましいアプリケーションの反対側からの変異であることが判明したときに、あいまいなバグを引き起こしている原因を解明しようとする実行のスレッド(同時実行タイプではない)を追いかけて何時間も費やしました!

不変性を使用すると、物事をlotで推論しやすくなります。 Xオブジェクトがその存続期間中に変更されるnotであり、変更する唯一の方法はそれを複製することです。可変性によってもたらされる可能性のあるマイクロ最適化よりも(特にチーム環境で)これをはるかに高く評価します。

例外がありますが、上記のデータ構造が最も顕著です。配列の場合と同じように、作成後にマップを変更したいというシナリオに遭遇することはめったにありません(確かに、ES6マップではなく疑似オブジェクトリテラルマップについて話しています)。より大きなデータ構造を扱う場合、可変性mightが効果を発揮します。 JavaScriptのすべてのオブジェクトは、値ではなく参照として渡されることに注意してください。


とはいえ、上記で指摘された1つの点は、GCと重複を検出できないことでした。これは正当な懸念事項ですが、私の意見では、メモリが懸念事項である場合にのみ懸念事項であり、たとえばクロージャ内の循環参照など、自分自身を隅にコーディングするはるかに簡単な方法があります。


最終的に、very変更可能なセクションがいくつかある場合は不変のコードベースを使用し、どこでも変更可能であるよりもパフォーマンスが少し低下することを好みます。何らかの理由で不変性がパフォーマンスの問題になる場合は、後でいつでも最適化できます。

5
Dan Pantry

直線的には、不変コードにはオブジェクト作成のオーバーヘッドがあり、速度は遅くなります。ただし、変更可能なコードを効率的に管理することが非常に困難になる状況も多くあり(結果として多くの防御的なコピーが発生し、コストも高くなります)、オブジェクトの「コピー」のコストを軽減するための巧妙な戦略が数多くあります。 、他の人が述べたように。

カウンターなどのオブジェクトがあり、1秒間に何回もインクリメントされる場合、そのカウンターを不変にしても、パフォーマンスが低下することはありません。アプリケーションのさまざまな部分で読み取られているオブジェクトがあり、それぞれがわずかに異なるオブジェクトのクローンを作成したい場合は、適切な方法でパフォーマンスを向上させることで、はるかに簡単に調整できます。不変オブジェクトの実装。

4
Dogs