web-dev-qa-db-ja.com

配列をトリミングできないのはなぜですか?

MSDNドキュメントサイトでは、Array.Resize 方法:

NewSizeが古い配列の長さより大きい場合、新しい配列が割り当てられ、すべての要素が古い配列から新しい配列にコピーされます。

NewSizeが古い配列の長さより小さい場合、新しい配列が割り当てられ、新しい配列がいっぱいになるまで要素が古い配列から新しい配列にコピーされます。古い配列の残りの要素は無視されます。

配列は、隣接するメモリブロックのシーケンスです。大きな配列が必要な場合、その隣のメモリが他のデータによって既に要求されている可能性があるため、メモリを追加できないことを理解しています。したがって、希望するより大きなサイズの隣接するメモリブロックの新しいシーケンスを要求し、そこにエントリをコピーし、古いスペースの要求を削除する必要があります。

しかし、なぜ小さなサイズの新しいアレイを作成するのでしょうか?アレイが最後のメモリブロックの主張を削除できないのはなぜですか?次に、現在のO(n)ではなくO(1)操作になります。

コンピュータのアーキテクチャレベルまたは物理レベルでデータがどのように編成されているかに関係がありますか?

70
Kjara

質問に答えるには、メモリ管理システムの設計に関係しています。

理論的には、独自のメモリシステムを記述している場合、あなたが言ったとおりに動作するように完全に設計できます。

問題は、そのように設計されなかった理由になります。答えは、メモリ管理システムがメモリの効率的な使用とパフォーマンスの間でトレードオフを行ったことです。

たとえば、ほとんどのメモリ管理システムは、バイト単位でメモリを管理しません。代わりに、メモリを8 KBのチャンクに分割します。これには多くの理由がありますが、そのほとんどはパフォーマンスに関するものです。

いくつかの理由は、プロセッサがメモリを移動する方法に関係しています。たとえば、4 KBをコピーするよりも、一度に8 KBのデータをコピーする方がプロセッサがはるかに優れていたとします。次に、データを8 KBのチャンクで保存するとパフォーマンスが向上します。これは、CPUアーキテクチャに基づいた設計上のトレードオフになります。

アルゴリズムのパフォーマンスのトレードオフもあります。たとえば、ほとんどのアプリケーションの動作を調べると、アプリケーションのサイズの6 KB〜8 KBのデータブロックが99%割り当てられていることがわかります。

メモリシステムで4 KBの割り当てと解放が許可されている場合、割り当ての99%が使用できない空き4 KBチャンクが残ります。 4 KBだけが必要な場合でも8 KBに過剰に割り当てられるのではなく、はるかに再利用可能です。

さらに別の設計を検討してください。任意のサイズの空きメモリロケーションのリストがあり、2KBのメモリを割り当てるように要求されたとします。 1つのアプローチは、空きメモリのリストを調べて少なくとも2KBのサイズのものを見つけることですが、リスト全体を調べてその最小ブロックを見つけるか、十分な大きさの最初のブロックを見つけて使用しますそれ。

最初のアプローチはより効率的ですが、低速で、2番目のアプローチは効率的ではありませんが高速です。

C#やJavaなどの「マネージドメモリ」を持つ言語でさらに興味深いものになります。マネージドメモリシステムでは、メモリは解放されず、使用が停止するだけです。後で、場合によってはずっと後に、検出して解放します。

さまざまなメモリ管理と割り当ての詳細については、Wikipediaのこの記事をご覧ください。

https://en.wikipedia.org/wiki/Memory_management

22
Luis Perez

未使用のメモリは実際には未使用ではありません。ヒープの穴を追跡することは、ヒープ実装の仕事です。少なくとも、マネージャーは穴のサイズを知る必要があり、その場所を追跡する必要があります。少なくとも8バイトは常にかかります。

.NETでは、System.Objectが重要な役割を果たします。誰もがそれが何をするのか、オブジェクトが収集された後も生き続けているほど明白ではないことを知っています。オブジェクトヘッダーの2つの追加フィールド(syncblockおよびtypeハンドル)は、前/次の空きブロックへの後方および前方ポインターに変わります。また、最小サイズは32ビットモードで12バイトです。オブジェクトが収集された後、常にフリーブロックサイズを保存するのに十分なスペースがあることを保証します。

そのため、おそらく問題が発生している可能性があります。配列のサイズを小さくしても、これらの3つのフィールドに適合する大きさの穴が作成されるとは限りません。 「できません」という例外をスローする以外に何もできません。プロセスのビット数にも依存します。考えるにはあまりにもugい。

35
Hans Passant

私はあなたの質問に対する答えを探していました。非常に興味深い質問でした。私は この答え を見つけました。これには興味深い最初の行があります:

配列の一部を解放することはできません-free()から取得したポインターはmalloc()しか使用できません。その場合、要求したすべての割り当てを解放します。

したがって、実際に問題になるのは、割り当てられたメモリを保持するレジスタです。割り当てたブロックの一部だけを解放することはできません。完全に解放する必要があるか、まったく解放しません。つまり、そのメモリを解放するには、最初にデータを移動する必要があります。 .NETメモリ管理がこの点で特別なことを行うかどうかはわかりませんが、このルールはCLRにも適用されると思います。

21
Patrick Hofman

これは、古い配列が破壊されないためだと思います。他の場所で参照されていて、アクセスできる場合は、まだそこにあります。これが、新しいアレイが新しいメモリの場所に作成される理由です。

例:

int[] original = new int[] { 1, 2, 3, 4, 5, 6 };
int[] otherReference = original; // currently points to the same object

Array.Resize(ref original, 3);

Console.WriteLine("---- OTHER REFERENCE-----");

for (int i = 0; i < otherReference.Length; i++)
{
    Console.WriteLine(i);
}

Console.WriteLine("---- ORIGINAL -----");

for (int i = 0; i < original.Length; i++)
{
    Console.WriteLine(i);
}

プリント:

---- OTHER REFERENCE-----
0
1
2
3
4
5
---- ORIGINAL -----
0
1
2
6
Zein Makki

Reallocをそのまま定義することには2つの理由があります。まず、より小さなサイズでreallocを呼び出しても同じポインタが返されるという保証がないことを完全に明確にします。プログラムがその前提を立てている場合、プログラムは壊れています。ポインターが同じ時間の99.99%であっても。たくさんの空きスペースの真ん中に大きなブロックの右スマックがあり、ヒープの断片化が発生している場合、reallocは可能であれば自由に移動できます。

第二に、これを行うことが絶対に必要な実装があります。たとえば、MacOS Xには、1つの大きなメモリブロックを使用して1から16バイトのmallocブロック、17から32バイトのmallocブロックに別の大きなメモリブロック、33から48バイトのmallocブロックに1つを割り当てる実装があります。これにより、サイズが33〜48バイトの範囲にとどまると、同じブロックが返されますが、32または49バイトに変更されるのは非常に自然ですmustブロックを再割り当てします。

Reallocのパフォーマンスの保証はありません。しかし、実際には、サイズを少し小さくすることはありません。主なケースは次のとおりです。必要なサイズの推定上限にメモリを割り当て、それを埋めてから、実際にはるかに小さい必要なサイズにサイズ変更します。または、メモリを割り当てて、不要になったら非常に小さなサイズに変更します。

5
gnasher729

多くのあらゆるヒープ管理システムで「内部」で動作する洗練されたデータ構造があるかもしれません。たとえば、現在のサイズに従ってブロックを保存します。ブロックに「分割、拡大、縮小」を許可した場合、a lotの合併症が追加されます。 (そして、それは本当に物事を「速く」することはありません。)

したがって、実装はalways -safe事を行います:新しいブロックを割り当て、必要に応じて値を移動します。 「この戦略は、どのシステムでも常に確実に機能する」ことが知られています。そして、それは本当に物事をまったく遅くしません。

3
Mike Robinson

内部では、配列は連続メモリブロックに格納されますが、多くの言語では依然としてプリミティブ型です。

質問に答えるために、配列に割り当てられたスペースは単一のブロックとみなされ、ローカル変数の場合はstackに、グローバルの場合はbss/data segmentsに格納されます。知る限り、array[3]などの配列に低レベルでアクセスすると、OSは最初の要素へのポインターを取得し、必要なブロックに到達するまで(上記の例では3回)ジャンプ/スキップします。 だから、一度宣言すると配列サイズを変更できないというのはアーキテクチャ上の決定かもしれません。

同様に、OSは必要なインデックスにアクセスする前に配列の有効なインデックスかどうかを知ることができません。 jumpingプロセスの後にメモリブロックに到達して要求されたインデックスにアクセスしようとして、到達したメモリブロックが配列の一部ではないことがわかると、Exceptionをスローします。

2
Vishnu Prasad V