このQ&Aの後半にはすでに素晴らしい回答がありますが、メモリ内のビットとバイトの下位レベルの観点から物事を見ることに慣れている外国人として侵入したかったのです。
Cの観点からでさえ、不変の設計に非常に興奮しています。最近のこの猛烈なハードウェアを効果的にプログラムする新しい方法を見つけるという観点からも。
遅い/速い
それが物事を遅くするかどうかの問題については、ロボットの答えは次のようになりますyes
。この種の非常に技術的な概念レベルでは、不変性は物事を遅くするだけです。ハードウェアは、散発的にメモリを割り当てるのではなく、代わりに既存のメモリを変更できる場合に最適です(なぜ一時的な局所性のような概念があるのか)。
しかし、実用的な答えはmaybe
です。パフォーマンスは、大部分の非自明なコードベースでは依然として生産性の指標です。バグを無視したとしても、通常、保守状態にある恐ろしいコードベースが競合状態で最も効率的であるとは思いません。多くの場合、効率は優雅さとシンプルさの関数です。マイクロ最適化のピークは多少矛盾する可能性がありますが、それらは通常、コードの最小で最も重要なセクションのために予約されています。
不変のビットとバイトの変換
低レベルの観点から言えば、X線などの概念を考えるobjects
とstrings
、その中心にあるのは、さまざまな速度/サイズの特性を持つさまざまな形式のメモリのビットとバイトだけです(メモリハードウェアの速度とサイズは通常相互に排他的)。
上記の図のように、同じメモリチャンクに繰り返しアクセスすると、コンピューターのメモリ階層は、頻繁にアクセスされるメモリチャンクを最高速のメモリ(L1キャッシュ、レジスタとほぼ同じ速度です)。まったく同じメモリに繰り返しアクセスする(複数回再利用する)か、チャンクの異なるセクションに繰り返しアクセスする(例:そのメモリチャンクのさまざまなセクションに繰り返しアクセスする連続したチャンクの要素をループする)。
このメモリを変更すると、次のようにまったく新しいメモリブロックを作成したい場合は、そのプロセスでレンチを投げます。
...この場合、新しいメモリブロックにアクセスするには、強制的にページフォールトとキャッシュミスを発生させて、メモリの最速形式(レジスタに至るまで)に戻す必要があります。それは本当のパフォーマンスキラーになり得ます。
これを緩和する方法はありますが、事前に割り当てられた事前に割り当てられたメモリの予約プールを使用します。
大きな集合体
少し高いレベルのビューから生じるもう1つの概念的な問題は、本当に大きな集合体の不必要なコピーを一括して行うことです。
過度に複雑な図を避けるために、この単純なメモリブロックが何らかの形で高価であると想像してみましょう(信じられないほど制限されたハードウェア上のUTF-32文字かもしれません)。
この場合、「HELP」を「KILL」に置き換え、このメモリブロックが不変であった場合、一部のみが変更されていても、新しいブロック全体を作成して一意の新しいオブジェクトを作成する必要があります。 :
私たちの想像力をかなり伸ばして、ほんの少しの部分をユニークにするために他のすべてのこの種の深いコピーは非常に高価かもしれません(実際のケースでは、このメモリブロックは問題を引き起こすためにはるかに大きくなります)。
ただし、このような費用にもかかわらず、この種の設計は人為的ミスがはるかに少ない傾向があります。純粋な関数を持つ関数型言語で働いたことのある人なら誰でもこれを高く評価できます。特に、このようなコードを世間を気にせずにマルチスレッド化できるマルチスレッドの場合は特にそうです。一般に、人間のプログラマーは、状態の変更、特に現在の関数のスコープ外の状態に外部の副作用を引き起こすものにつまずく傾向があります。そのような場合の外部エラー(例外)からの回復でさえ、ミックス内の外部状態が変化する可能性があるため、非常に困難です。
この冗長なコピー作業を軽減する1つの方法は、次のように、これらのメモリブロックを文字へのポインター(または参照)のコレクションにすることです。
申し訳ありませんがL
、図を作成するときに一意にする必要がないことに気づきませんでした。
青は浅いコピーされたデータを示します。
...残念ながら、これは文字ごとにポインタ/参照コストを支払うと信じられないほど高価になります。さらに、文字の内容をアドレススペース全体に散らばって、ページフォールトとキャッシュミスのボートロードという形で支払うことになり、このソリューション全体をコピーするよりもさらに悪化する可能性があります。
これらの文字を連続して割り当てるように注意していたとしても、マシンが8つの文字と1つの文字への8つのポインターをキャッシュラインにロードできるとしましょう。新しい文字列をたどるために、このようなメモリをロードすることになります。
この場合、理想的には3つだけが必要なときに、この文字列をトラバースするために、7つの異なるキャッシュラインに相当する連続したメモリをロードする必要があります。
データをチャンクアップする
上記の問題を軽減するために、同じ基本戦略を8文字のより粗いレベルで適用できます。
その結果、この文字列をトラバースするためにロードされる4キャッシュライン分のデータ(3つのポインターに1つ、文字に3つ)が必要になります。
だからそれはまったく悪くない。メモリの浪費はありますが、メモリは十分にあり、余分なメモリが頻繁にアクセスされないコールドデータになる場合、メモリを使い切っても速度が低下することはありません。メモリーの使用量と速度の低下が頻繁に発生し、より多くのメモリーを単一のページまたはキャッシュ行に入れて、追い出す前にすべてにアクセスしたい場合は、ホットで連続したデータのみです。この表現はかなりキャッシュに優しいです。
速度
したがって、上記のような表現を使用すると、パフォーマンスのバランスがかなり良くなります。おそらく、不変のデータ構造の最もパフォーマンスが重要な使用法は、チャンクなデータを変更し、プロセス内で一意にする一方で、変更されていないデータを浅くコピーするという性質を引き継ぎます。また、マルチスレッドコンテキストで浅いコピー部分を安全に参照するためのアトミック操作のオーバーヘッドを意味します(アトミックな参照カウントが行われている場合があります)。
しかし、これらのチャンキーなデータが十分に粗いレベルで表現されている限り、このオーバーヘッドの多くは減少し、場合によってはささいなことさえあります。エフェクト。
新しいデータと古いデータを保持する
パフォーマンスの観点から(実用的な意味で)不変性が最も役立つ可能性があると思うのは、大きなデータのコピー全体を作成して、新しいデータを生成することを目標とする可変コンテキストでユニークにしたい場合です。新しいものと古いものの両方を保持したい方法で既に存在しているもので、慎重に不変のデザインでほんの少しだけユニークなものにすることができます。
例:システムを元に戻す
この例は、取り消しシステムです。データ構造の一部を変更し、元に戻すことができる元のフォームと新しいフォームの両方を保持したい場合があります。データ構造の小さな変更されたセクションのみを一意にするこの種の不変の設計により、追加された一意の部分データのメモリコストを支払うだけで、古いデータのコピーを元に戻すエントリに保存できます。これにより、生産性(Undoシステムの実装を簡単にする)とパフォーマンスの非常に効果的なバランスが提供されます。
高レベルのインターフェース
しかし、上記の場合には厄介なことが発生します。ローカルな種類の関数コンテキストでは、多くの場合、変更可能なデータが最も簡単で簡単に変更できます。結局のところ、配列を変更する最も簡単な方法は、多くの場合、ループを介して一度に1つの要素を変更することです。配列を変換するために選択する多数の高レベルのアルゴリズムがあり、適切なアルゴリズムを選択して、変更された部分がすべての間にこれらのチャンキーな浅いコピーがすべて作成されるようにする必要がある場合、知的オーバーヘッドが増加する可能性がありますユニークにしました。
おそらく、これらの場合の最も簡単な方法は、関数のコンテキスト内でローカルに可変バッファーを使用することです(通常、それらはトリップしません)、データ構造に変更を原子的にコミットして、新しい不変のコピーを取得します(一部の言語ではこれらの「トランジェント」)...
...または、単純にデータの上位および上位レベルの変換関数をモデル化して、可変バッファーを変更し、可変ロジックを使用せずに構造にコミットするプロセスを隠すことができます。いずれにせよ、これはまだ広く調査された領域ではなく、これらのデータ構造を変換するための意味のあるインターフェースを考え出すために不変の設計をさらに取り入れる場合、作業が中断されます。
データ構造
ここで発生するもう1つのことは、パフォーマンスが重要なコンテキストで使用される不変性により、データ構造がチャンクのサイズが小さすぎず、大きすぎないチャンキーデータに分割される可能性があることです。
リンクされたリストは、これに対応し、展開されたリストに変換するために、かなり変更したい場合があります。大きくて連続した配列は、ランダムアクセス用のモジュロインデックスを使用して、連続したチャンクへのポインタの配列に変わる可能性があります。
興味深い方法でデータ構造の見方を変える可能性がありますが、これらのデータ構造の変更機能をより大きな性質に似せて、ここでいくつかのビットを浅くコピーし、他のビットをそこに一意にする余分な複雑さを隠します。
性能
とにかく、これはこのトピックについての私の低レベルのビューです。理論的には、不変性のコストは非常に大きくても小さくてもかまいません。しかし、非常に理論的なアプローチでは、アプリケーションが常に高速になるとは限りません。それらをスケーラブルにするかもしれませんが、実際の速度では、より実用的な考え方を受け入れる必要があります。
実用的な観点から、パフォーマンス、保守性、安全性などの品質は、特に非常に大きなコードベースの場合、1つの大きな不鮮明になる傾向があります。絶対的な意味でのパフォーマンスは不変性によって低下しますが、生産性と安全性(スレッドの安全性を含む)にもたらす利点を議論することは困難です。開発者がバグに悩まされることなくコードを調整および最適化するためのより多くの時間があるという理由だけで、これらの増加により実際のパフォーマンスが向上することがよくあります。
だから私は、この実用的な意味から、不変のデータ構造は、実際にかもしれないと思う助け、それは音として奇数として、多くのケースでパフォーマンスを。理想的な世界では、不変データ構造と不変データ構造の2つの組み合わせが求められます。不変データ構造は通常、非常にローカルなスコープ(例:関数に対してローカル)で安全に使用できます。完全に影響し、データ構造へのすべての変更をアトミック操作に変換して、競合状態のリスクのない新しいバージョンを生成します。