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


88

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

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

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

私が見逃している不変性にパフォーマンス上の利点はありますか?


8
不変性(場合によっては)にパフォーマンスコストがあり、パフォーマンスコストを可能な限り最小限に抑えたいため、パフォーマンスに大きな懸念があります。不変性は、それ自体がすべて、マルチスレッドコードの記述を容易にするという意味でのみ、パフォーマンス上の利点があります。
ロバートハーベイ

8
私の経験では、パフォーマンスは2つのシナリオで有効な懸念事項です-1つは1秒で30回以上アクションが実行される場合、2つは実行ごとにその効果が増加する場合(Windows XPではWindows Update時間がかかるバグが見つかりました履歴のすべての更新O(pow(n, 2))に対して。)他のほとんどのコードは、イベントへの即時応答です。クリック、APIリクエストなどがあり、実行時間が一定である限り、任意の数のオブジェクトのクリーンアップはほとんど問題になりません。
Katana314

4
また、不変データ構造の効率的な実装が存在することを考慮してください。たぶん、これらは変更可能なものほど効率的ではありませんが、おそらく単純な実装よりもさらに効率的です。例:クリス・オカサキによる純粋に機能的なデータ構造
ジョルジオ

1
@ Katana314:私にとって30回以上は、パフォーマンスの心配を正当化するのにまだ十分ではありません。作成した小さなCPUエミュレーターをnode.jsに移植し、nodeは約20MHzで仮想CPUを実行しました(1秒間に2000万回)。したがって、パフォーマンスを心配するのは、1秒間に1000回以上の操作を行っている場合だけです(その場合でも、1秒間に10を超える操作を快適に行えることがわかっているので、1秒間に1000000の操作を行うまで心配することはありません) 。
スリーブマン

2
@RobertHarvey「不変性は、それ自体で、マルチスレッドコードの記述を容易にするという意味でのみパフォーマンス上の利点があります。」それは完全に真実ではありません、不変性は実際の結果なしで非常に普及した共有を可能にします。これは、可変環境では非常に安全ではありません。これは、あなたは次のように考えていますO(1)、配列のスライスとO(log n)、まだ自由に古いものを使用することができながら、バイナリツリーに挿入すると、別の例では、されtails、リストのすべての尾をとるtails [1, 2] = [[1, 2], [2], []]かかるだけO(n)の時間と空間をしているが、O(n^2)要素にカウント
セミコロン

回答:


59

たとえば、オブジェクトの単一のプロパティを変更する必要がある場合は、新しいプロパティを使用して新しいオブジェクト全体を作成し、古いオブジェクトから他のすべてのプロパティをコピーして、古いオブジェクトをガベージコレクトしてください。

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

これが示すことは、正しい仮想シナリオを作成すれば、特にパフォーマンスに関しては何でも証明できます。しかし、私の例は、聞こえるかもしれないほど仮説的ではありません。私は先月、不変のデータ構造の使用に最初に決めたため、その問題につまずいたプログラムで作業しましたが、面倒な価値がないと思われるため、後でこれをリファクタリングすることにheしました。

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

最後の注意点:実際に遭遇する可能性のある問題は、いくつかの基本的なデータ構造の不変性の有無を早期に判断する必要があるということです。次に、その上に大量のコードを作成し、数週間後または数か月後に、決定が良いものか悪いものかを確認します。

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

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

これらの両極端の間の状況では、あなたの判断を使用してください。しかし、YMMV。


8
That will leave a lot more garbage than in your case.さらに悪いことに、ランタイムは無意味な重複を検出できない可能性が高いため、(誰も使用していない期限切れの不変オブジェクトとは異なり)収集することさえできません。
ジェイコブライレ

37

まず、不変のデータ構造の特性評価は不正確です。一般に、データ構造の大部分はコピーされず、共有され、変更された部分のみがコピーされます。永続データ構造と呼ばれます。ほとんどの実装は、ほとんどの場合、永続的なデータ構造を利用できます。パフォーマンスは、機能プログラマが一般的に無視できると考える可変データ構造に十分に近いです。

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

私は、アルゴリズムがどれだけのゴミを作成するかについて教える関数型プログラミングのクラスで人々に意見を述べました。そして、同じくらい作成する同じアルゴリズムの典型的な命令型バージョンを示します。どういうわけか、人々はもう気づかない。

変数を有効な値にするまで共有を奨励し、変数を作成しないようにすることで、不変性は、よりクリーンなコーディングの実践と長寿命のデータ構造を促進する傾向があります。これにより、ご使用のアルゴリズムに応じて、多くの場合、ガベージのレベルは低くなりますが、同等になります。


8
「...それから、同じアルゴリズムを作成する典型的な命令型バージョンを示します。」この。また、このスタイルが初めての人、特に一般的な機能スタイルが初めての人は、最初は次善の機能実装を作成する可能性があります。
wberry

1
「変数の作成の回避」これは、デフォルトの動作が代入/暗黙的構築のコピーである言語にのみ有効ではありませんか?JavaScriptでは、変数は単なる識別子です。それ自体がオブジェクトではありません。それはまだどこかにスペースを占有しますが、それはごくわずかです(特に私の知る限り、JavaScriptのほとんどの実装ではまだ関数呼び出しにスタックを使用しています。つまり、再帰がたくさんない限り、ほとんどの場合同じスタックスペースを再利用することになります)一時変数)。不変性はその側面とは関係ありません。
JAB

33

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

Cの観点からでさえ、不変の設計に非常に興奮しています。最近のこの猛烈なハードウェアを効果的にプログラムする新しい方法を見つけるという観点からも。

遅い/速い

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

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

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

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

ここに画像の説明を入力してください

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

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

ここに画像の説明を入力してください

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

これを緩和する方法はありますが、事前に割り当てられた事前に割り当てられたメモリの予約プールを使用します。

大きな集合体

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

過度に複雑な図を避けるために、この単純なメモリブロックが何らかの形で高価であると想像してみましょう(信じられないほど制限されたハードウェア上のUTF-32文字かもしれません)。

ここに画像の説明を入力してください

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

ここに画像の説明を入力してください

私たちの想像力をかなり伸ばして、ほんの少しの部分をユニークにするために他のすべてのこの種の深いコピーは非常に高価かもしれません(実際のケースでは、このメモリブロックは問題を引き起こすためにはるかに大きくなります)。

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

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

申し訳ありませんがL、図を作成するときに一意にする必要がないことに気づきませんでした。

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

ここに画像の説明を入力してください

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

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

ここに画像の説明を入力してください

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

データをチャンクアップする

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

ここに画像の説明を入力してください

その結果、この文字列をトラバースするためにロードされる4キャッシュライン分のデータ(3つのポインターに1つ、文字に3つ)が必要になります。

だからそれはまったく悪くない。メモリの浪費はありますが、メモリは十分にあり、余分なメモリが頻繁にアクセスされないコールドデータになる場合、メモリを使い切っても速度が低下することはありません。メモリーの使用量と速度の低下が頻繁に発生し、より多くのメモリーを単一のページまたはキャッシュ行に入れて、追い出す前にすべてにアクセスしたい場合は、ホットで連続したデータのみです。この表現はかなりキャッシュに優しいです。

速度

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

しかし、これらのチャンキーなデータが十分に粗いレベルで表現されている限り、このオーバーヘッドの多くは減少し、場合によってはささいなことさえあります。エフェクト。

新しいデータと古いデータを保持する

パフォーマンスの観点から(実用的な意味で)不変性が最も役立つ可能性があると思うのは、大きなデータのコピー全体を作成して、新しいデータを生成することを目標とする可変コンテキストでユニークにしたい場合です。新しいものと古いものの両方を保持したい方法で既に存在しているもので、慎重に不変のデザインでほんの少しだけユニークなものにすることができます。

例:システムを元に戻す

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

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

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

おそらく、これらの場合の最も簡単な方法は、関数のコンテキスト内でローカルに可変バッファーを使用することです(通常、それらはトリップしません)、データ構造に変更を原子的にコミットして、新しい不変のコピーを取得します(一部の言語ではこれらの「トランジェント」)...

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

データ構造

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

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

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

性能

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

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

だから私は、この実用的な意味から、不変のデータ構造は、実際にかもしれないと思う助け、それは音として奇数として、多くのケースでパフォーマンスを。理想的な世界では、不変データ構造と不変データ構造の2つの組み合わせが求められます。不変データ構造は通常、非常にローカルなスコープ(例:関数に対してローカル)で安全に使用できます。完全に影響し、データ構造へのすべての変更をアトミック操作に変換して、競合状態のリスクのない新しいバージョンを生成します。


11

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操作できる関数に渡す前に防御的にコピーすることで同じ保証を提供しようとすると、パフォーマンスが著しく向上します。


4

直線的には、不変コードにはオブジェクト作成のオーバーヘッドがあり、これはより低速です。ただし、可変コードを効率的に管理するのが非常に困難になる状況が多くあり(結果として多くの防御コピーが発生します) 、他の人が述べたように。

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


4

この(すでに優れた回答が寄せられている)質問に追加するには:

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


しかし、長い答えは本当にありません

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

私にとって最大の「パフォーマンス」の向上は、ランタイムのパフォーマンスではなく、開発者のパフォーマンスです。Real World(tm)アプリケーションでの作業中に私が最初に学んだことの1つは、可変性は本当に危険で混乱しやすいということです。私は、実行中のスレッド(同時実行型ではない)を追いかけて何時間も失ってしまいました。

不変性を使用すると、物事の推論がはるかに簡単になります。Xオブジェクトはその存続期間中に変更されないことをすぐに知ることができ、変更するための唯一の方法はそれを複製することです。これは、可変性がもたらす可能性のあるマイクロ最適化よりもずっと重要です(特にチーム環境)。

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


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


最終的に、変更可能なセクションが非常に少ない(存在する場合)不変のコードベースを持ち、どこにでも変更可能性があるよりもパフォーマンスがわずかに低いことを望みます。何らかの理由で不変性がパフォーマンスの問題になる場合は、後からいつでも最適化できます。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.