7

最初我想知道是否ToList分配了比使用构造函数更多的内存List<T>IEnumerable<T>没有区别)。

出于测试目的,我曾经Enumerable.Range创建一个源数组,我可以使用它来创建List<int>via 1.ToList和 2. constructor的实例。两者都在创建副本。

这就是我注意到以下之间内存消耗的巨大差异的原因:

  1. Enumerable.Range(1, 10000000)或者
  2. Enumerable.Range(1, 10000000).ToArray()

当我使用第一个并调用ToList生成的对象时,需要比数组(38,26MB/64MB)多出约 60% 的内存。

问:这是什么原因,或者我的推理错误在哪里?

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>];因此它可以分配一个数组,第一次调整大小,然后块复制内容。

对于序列,这是不可能的(序列不会以任何可访问的方式公开长度);因此它必须改为回退到“继续填满缓冲区;如果已满,则将其翻倍并复制”。

显然,这需要大约两倍的内存。

一个有趣的测试是:

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

这将在最初分配正确的大小,同时仍使用序列。

tl;博士; 构造函数在传递一个序列时,首先检查它是否可以通过转换为众所周知的接口来获得长度。

于 2012-05-09T15:36:58.923 回答
3

这是因为用于在列表中创建后备数组的加倍算法。IEnumerable 没有 Count 属性,因此当您调用 ToList 时,它无法将支持数组预分配为目标大小。事实上,在每次调用 MoveNext 时,您都在调用 List 上的相应 Add。

但是 Array.ToList 可以覆盖基本 ToList 行为以将列表初始化为正确的容量。此外,它可能是它的构造函数中的 List,它试图将 IEnumerable 引用向下转换为已知的集合类型,例如 IList、ICollection、Array 等......

更新

事实上,它是在 List 的构造函数中确定参数是否实现了 ICollection:

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 回答
3

List is implemented as an array. When you exceed what it has allocated, it allocates another array double the size (essentially doubling the memory allocation). The default capacity is 4, and it doubles things from here on.

Most likely if you drop the number of items to say 7,500, you'll see the array drop to a little under 32 MBs, and the IList size to be 32 MBs.

You can tell IList<T> what the initial size should be, which is why if you give it the IEnumerable<T> at construction time, it shouldn't over allocate memory.

[Edit] after comments

In the case of Enumerable.Range(a, b) it returns an IEnumerable<T> only rather than an ICollection<T>. For List<T> to not overallocate the item passed during construction must also be an ICollection<T>

于 2012-05-09T15:34:24.370 回答
0

我猜是:

  • Enumerable.Range(1, 10000000)只创建一个 IEnumerable 并且不创建项目。

  • Enumerable.Range(1, 10000000).ToArray()创建一个数组,将内存用于数字

  • Enumerable.Range(1, 10000000).ToList()创建数字和附加数据来管理列表(部分之间的链接。列表可以更改其大小并需要以块为单位分配内存)。

于 2012-05-09T15:37:40.203 回答