web-dev-qa-db-ja.com

C#はどのようにしてList <T>にメモリを動的に割り当てますか?

LukeH's から c#のlist <string>へのデータの最大制限はいくつですか?

Listの現在の実装に格納できる要素の最大数は、理論的にはInt32.MaxValueであり、20億をわずかに超えています。

リストには大量のアイテムを含めることができることがわかります。コンパイラーは、List<T>の新しい実装ごとに、Tの20億倍のサイズのスペースを解放するだけではないと想定しています。では、リストはどのように動的に大きくなるのでしょうか。メモリ内の連続していないスペースへのポインタはありますか?

10
8protons

_List<T>_ クラスは、内部の_T[]_配列を内部で使用するように実装されています。 List<T>(int) コンストラクターを使用して初期化すると、指定されたサイズの配列が割り当てられます。デフォルトのコンストラクターを使用すると、デフォルトの容量4になりますが、この場合、配列は最初の追加でのみ割り当てられます。

リストに要素を追加するたびに、最初に容量に達しているかどうか(つまり、既存のCountCapacityと等しいかどうか)がチェックされます。その場合、前の配列の2倍のサイズの新しい配列を作成し、既存のすべての要素をその配列にコピーしてから、新しい要素の書き込みを続行します。これは、参照したハード制限(_Int32.MaxValue_)に達するまで、後続の要素の追加で無期限に発生し続けます。

パフォーマンス面では、これは、要素の追加が、容量に応じてO(1)またはO(n)操作)のいずれかであることを意味します増やす必要があります( Add で説明されています)。ただし、容量は2倍であるため、増加する必要がある場合、この再割り当ては、リストが大きくなるにつれて指数関数的に減少する頻度で発生します。たとえば、4から開始すると、容量の増加は4、8、16、32、64、128、…要素で発生します。したがって、総コストAddをn回呼び出すときの再割り当ての割合は、およそ4 + 8 + 16 +…+ n/8 + n/4 + n/2になります。これは、依然としてO(n)に対応します。

一連の加算操作に沿った内部配列の状態を示す例を次に示します。

_                               //   ┌┐
var list = new List<char>();   //   ││   Count:    0
                               //   └┘   Capacity: 0
                               //   ┌───┬───┬───┬───┐
list.Add('h');                 //   │ h │ ░ │ ░ │ ░ │   Count:    1
                               //   └───┴───┴───┴───┘   Capacity: 4
                               //   ┌───┬───┬───┬───┐
list.Add('e');                 //   │ h │ e │ ░ │ ░ │   Count:    2
                               //   └───┴───┴───┴───┘   Capacity: 4
                               //   ┌───┬───┬───┬───┐
list.Add('l');                 //   │ h │ e │ l │ ░ │   Count:    3
                               //   └───┴───┴───┴───┘   Capacity: 4
                               //   ┌───┬───┬───┬───┐
list.Add('l');                 //   │ h │ e │ l │ l │   Count:    4
                               //   └───┴───┴───┴───┘   Capacity: 4
                               //   ┌───┬───┬───┬───┬───┬───┬───┬───┐
list.Add('o');                 //   │ h │ e │ l │ l │ o │ ░ │ ░ │ ░ │   Count:    5
                               //   └───┴───┴───┴───┴───┴───┴───┴───┘   Capacity: 8
                               //   ┌───┬───┬───┬───┬───┬───┬───┬───┐
list.Add(' ');                 //   │ h │ e │ l │ l │ o │   │ ░ │ ░ │   Count:    6
                               //   └───┴───┴───┴───┴───┴───┴───┴───┘   Capacity: 8
                               //   ┌───┬───┬───┬───┬───┬───┬───┬───┐
list.Add('w');                 //   │ h │ e │ l │ l │ o │   │ w │ ░ │   Count:    7
                               //   └───┴───┴───┴───┴───┴───┴───┴───┘   Capacity: 8
                               //   ┌───┬───┬───┬───┬───┬───┬───┬───┐
list.Add('o');                 //   │ h │ e │ l │ l │ o │   │ w │ o │   Count:    8
                               //   └───┴───┴───┴───┴───┴───┴───┴───┘   Capacity: 8
                               //   ┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┐
list.Add('r');                 //   │ h │ e │ l │ l │ o │   │ w │ o │ r │ ░ │ ░ │ ░ │ ░ │ ░ │ ░ │ ░ │   Count:    9
                               //   └───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┘   Capacity: 16
                               //   ┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┐
list.Add('l');                 //   │ h │ e │ l │ l │ o │   │ w │ o │ r │ ░ │ ░ │ ░ │ ░ │ ░ │ ░ │ ░ │   Count:    10
                               //   └───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┘   Capacity: 16
                               //   ┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┐
list.Add('d');                 //   │ h │ e │ l │ l │ o │   │ w │ o │ r │ l │ d │ ░ │ ░ │ ░ │ ░ │ ░ │   Count:    11
                               //   └───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┘   Capacity: 16
_

__記号は、まだ使用されていない割り当て済みスペースを表します。これらの配列の場所には、Tの-​​ デフォルト値 が含まれます。 charの場合、これはヌル文字_\0_になります。ただし、これらの値は消費者には決して見えません。

AddRange を介して複数の要素を一緒に追加する場合、最大で1つの再割り当てのみが実行されます。以前の容量を2倍にすると、すべての新しい要素に対応するには不十分な場合は、代わりに内部配列がすぐに新しいカウントに増加します。

追加とは異なり、要素を削除してもリストが自動的に縮小されることはありません。ただし、 TrimExcess を呼び出すことにより、これを手動で発生させることができます。

コメント で述べたように、上記のいくつかの側面(デフォルトの初期容量4など)は、.NET Framework 4.7.2の ソースコード から派生した実装の詳細です。 。ただし、コア原則は十分に定着しており、他の/将来のフレームワークで変更される可能性はほとんどありません。

23
Douglas

あなたの仮定は正しいです、コンパイラは何も割り当てません。 List<T>クラスは内部的に配列を使用して要素を格納し、Addを呼び出すたびに、配列のサイズが十分であるかどうかをチェックします。これは、次のようになります。 ソースコード内

public void Add(T item) {
    if (_size == _items.Length) EnsureCapacity(_size + 1);
    _items[_size++] = item;
    _version++;
}

private void EnsureCapacity(int min) {
    if (_items.Length < min) {
        int newCapacity = _items.Length == 0? _defaultCapacity : _items.Length * 2;
        // Allow the list to grow to maximum possible capacity (~2G elements) before encountering overflow.
        // Note that this check works even when _items.Length overflowed thanks to the (uint) cast
        if ((uint)newCapacity > Array.MaxArrayLength) newCapacity = Array.MaxArrayLength;
        if (newCapacity < min) newCapacity = min;
        Capacity = newCapacity;
    }
}
5

これが具体的にどのように達成されるかについては、 @ CamiloTerevintoの回答 のようにソースコードが決定的ですが、ドキュメントでもこれについて説明しています。

List<>クラスの備考セクション は次のように述べています。

Listクラスは、ArrayListクラスの一般的な同等物です。必要に応じてサイズが動的に増加する配列を使用して、IListジェネリックインターフェイスを実装します。

Capacityプロパティの備考セクション は次のように詳しく説明しています。

容量は、サイズ変更が必要になる前にリストに保存できる要素の数ですが、カウントは、実際にリストにある要素の数です。

容量は常にカウント以上です。要素の追加中にカウントが容量を超えた場合、古い要素をコピーして新しい要素を追加する前に内部配列を自動的に再割り当てすることにより、容量が増加します。

3
BACON