7

もともと私はToList、コンストラクターを使用するよりも多くのメモリーを割り当てるかどうかを知りたいと思っていましList<T>IEnumerable<T>(違いはありません)。

テストの目的で、1。および2.コンストラクターを介しEnumerable.Rangeてインスタンスを作成するために使用できるソース配列を作成するために使用しました。どちらもコピーを作成しています。List<int>ToList

このようにして、次のメモリ消費量に大きな違いがあることに気づきました。

  1. Enumerable.Range(1, 10000000)また
  2. Enumerable.Range(1, 10000000).ToArray()

最初のオブジェクトを使用しToListて結果のオブジェクトを呼び出すと、配列(38,26MB / 64MB)よりも最大60%多くのメモリが必要になります。

Q:これの理由は何ですか、または推論の誤りはどこにありますか?

var memoryBefore = GC.GetTotalMemory(true);
var range = Enumerable.Range(1, 10000000);
var rangeMem = GC.GetTotalMemory(true) - memoryBefore; // negligible
var list = range.ToList();
var memoryList = GC.GetTotalMemory(true) - memoryBefore - rangeMem;

String memInfoEnumerable = String.Format("Memory before: {0:N2} MB List: {1:N2} MB"
    , (memoryBefore / 1024f) / 1024f
    , (memoryList   / 1024f) / 1024f);
// "Memory before: 0,11 MB List: 64,00 MB"

memoryBefore = GC.GetTotalMemory(true);
var array = Enumerable.Range(1, 10000000).ToArray();
var memoryArray = GC.GetTotalMemory(true) - memoryBefore;
list = array.ToList();
memoryList = GC.GetTotalMemory(true) - memoryArray;

String memInfoArray = String.Format("Memory before: {0:N2} MB Array: {1:N2} MB List: {2:N2} MB"
   , (memoryBefore / 1024f) / 1024f
   , (memoryArray  / 1024f) / 1024f
   , (memoryList   / 1024f) / 1024f);
// "Memory before: 64,11 MB Array: 38,15 MB List: 38,26 MB"
4

4 に答える 4

13

これはおそらく、リストに追加するときにバッキングバッファのサイズを変更するために使用されるダブリングアルゴリズムに関連しています。配列として割り当てる場合、その長さは既知IList[<T>]であり、および/またはICollection[<T>];をチェックすることで照会できます。したがって、最初に適切なサイズの単一の配列を割り当ててから、内容をブロックコピーするだけで済みます。

シーケンスではこれは不可能です(シーケンスはアクセス可能な方法で長さを公開しません)。したがって、代わりに「バッファをいっぱいにし続ける。いっぱいになったら、それを2倍にしてコピーする」にフォールバックする必要があります。

明らかに、これには約2倍のメモリが必要です。

興味深いテストは次のとおりです。

var list = new List<int>(10000000);
list.AddRange(Enumerable.Range(1, 10000000));

これにより、シーケンスを使用しながら、最初に適切なサイズが割り当てられます。

tl; dr; コンストラクターは、シーケンスが渡されると、最初に、既知のインターフェースにキャストすることによって長さを取得できるかどうかを確認します。

于 2012-05-09T15:36:58.923 に答える
3

リストは配列として実装されます。割り当てられた量を超えると、2倍のサイズの別の配列が割り当てられます(基本的にメモリ割り当てが2倍になります)。デフォルトの容量は4で、今後は2倍になります。

ほとんどの場合、アイテムの数を7,500に落とすと、配列が32 MB弱に下がり、IListサイズが32MBになります。

初期サイズがIList<T>どうあるべきかがわかります。そのためIEnumerable<T>、構築時に指定した場合、メモリを過剰に割り当ててはなりません。

コメント後[編集]

その場合は、ではなくEnumerable.Range(a, b)、のみを返します。建設中に渡されたアイテムを過剰に割り当てないためには、IEnumerable<T>ICollection<T>List<T>ICollection<T>

于 2012-05-09T15:34:24.370 に答える
3

これは、リストにバッキング配列を作成するために使用されるダブリングアルゴリズムが原因です。IEnumerableにはCountプロパティがないため、ToListを呼び出すときにバッキング配列をターゲットサイズに事前に割り当てることはできません。実際、MoveNextを呼び出すたびに、対応するリストの追加を呼び出しています。

ただし、Array.ToListは、基本のToList動作をオーバーライドして、リストを正しい容量に初期化できます。また、IList、ICollection、Arrayなどの既知のコレクションタイプへのIEnumerable参照をダウンキャストしようとするコンストラクター内のリストである可能性もあります。

アップデート

実際、引数がICollectionを実装しているかどうかを判断するのは、Listのコンストラクターです。

public List(IEnumerable<T> collection)
{
  if (collection == null)
    ThrowHelper.ThrowArgumentNullException(ExceptionArgument.collection);
  ICollection<T> collection1 = collection as ICollection<T>;
  if (collection1 != null)
  {
    int count = collection1.Count;
    if (count == 0)
    {
      this._items = List<T>._emptyArray;
    }
    else
    {
      this._items = new T[count];
      collection1.CopyTo(this._items, 0);
      this._size = count;
    }
  }
  else
  {
    this._size = 0;
    this._items = List<T>._emptyArray;
    foreach (T obj in collection)
      this.Add(obj);
  }
}
于 2012-05-09T15:38:13.417 に答える
0

私はそれを推測します:

  • Enumerable.Range(1, 10000000)IEnumerableを作成するだけで、まだアイテムを作成しません。

  • Enumerable.Range(1, 10000000).ToArray()数値にメモリを使用して配列を作成します

  • Enumerable.Range(1, 10000000).ToList()リストを管理するための番号と追加データを作成します(パーツ間のリンク。リストはサイズを変更する可能性があり、メモリをブロック単位で割り当てる必要があります)。

于 2012-05-09T15:37:40.203 に答える