使用Enumerable.Range消耗大量内存?

时间:2012-05-09 15:30:37

标签: c# .net linq memory-management

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

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

这就是我注意到内存消耗的巨大差异:

  1. Enumerable.Range(1, 10000000)
  2. Enumerable.Range(1, 10000000).ToArray()
  3. 当我使用第一个并调用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 个答案:

答案 0 :(得分:13)

这可能与添加到列表时用于调整后备缓冲区大小的加倍算法有关。当您分配为数组时,的长度已知,可以通过检查IList[<T>]和/或ICollection[<T>]来查询;因此它可以分配单个数组,第一次调整大小,然后只是块内容复制。

对于序列,这是不可能的(序列不以任何可访问的方式暴露长度);因此它必须反而回到“继续填充缓冲区;如果已满,则加倍并复制”。

显然这需要大约两倍的记忆。

一个有趣的测试是:

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

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

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

答案 1 :(得分:3)

列表实现为数组。当你超过它分配的内容时,它会分配另一个大小加倍的数组(基本上是内存分配的两倍)。默认容量为4,它从这里开始加倍。

最有可能的是,如果您将项目数量减少到7,500,您将看到数组下降到32 MB以下,IList大小为32 MB。

您可以告诉IList<T>初始大小应该是什么,这就是为什么如果您在构建时给它IEnumerable<T>不应该过度分配内存。

评论后的[编辑]

如果是Enumerable.Range(a, b),则仅返回IEnumerable<T>而不是ICollection<T>。对于List<T>,不要过度定位在构造期间传递的项目也必须是ICollection<T>

答案 2 :(得分:3)

这是因为用于在List中创建支持数组的加倍算法。 IEnumerable没有Count属性,因此当您调用ToList时,它无法将支持数组预先分配为目标大小。实际上,在每次调用MoveNext时,您都会在列表中调用相应的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);
  }
}

答案 3 :(得分:0)

我想:

  • Enumerable.Range(1, 10000000)仅创建IEnumerable,但尚未创建项目。

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

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