Enumerable.Range 的高内存消耗?

Posted

技术标签:

【中文标题】Enumerable.Range 的高内存消耗?【英文标题】:High memory consumption with Enumerable.Range? 【发布时间】:2012-05-18 03:45:10 【问题描述】:

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

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

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

    Enumerable.Range(1, 10000000)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"

【问题讨论】:

仅供参考,您也可以在第 5 行调用 list.TrimExcess();,而不是将列表初始化为确切大小。 @Marc:是的,但您首先需要知道它在这里可能有用。正如 Marc Gravell 所指出的,另一种方法是使用 range.Count() 初始化列表,然后使用 AddRange(range) 【参考方案1】:

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

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

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

一个有趣的测试是:

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

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

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

【讨论】:

【参考方案2】:

这是因为用于在 List 中创建后备数组的加倍算法。 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);
  

【讨论】:

【参考方案3】:

List 被实现为一个数组。当您超过分配的内容时,它会分配另一个数组,其大小是原来的两倍(基本上是内存分配的两倍)。默认容量为 4,从现在开始它会翻倍。

如果您将项目数减少到 7,500 个,您将看到数组减少到略低于 32 MB,并且 IList 大小为 32 MB。

您可以告诉IList&lt;T&gt; 初始大小应该是多少,这就是为什么如果您在构造时给它IEnumerable&lt;T&gt;,它不应该过度分配内存。

在cmets之后[编辑]

对于Enumerable.Range(a, b),它只返回IEnumerable&lt;T&gt;,而不是ICollection&lt;T&gt;。对于List&lt;T&gt; 不过度分配在构造过程中传递的项目也必须是ICollection&lt;T&gt;

【讨论】:

which is why if you give it the IEnumerable&lt;T&gt; at construction time, it shouldn't over allocate memory. 这是不正确的。只有当IEnumerable&lt;T&gt; 也是ICollection&lt;T&gt; 时,它才不会过度分配 @Marc 值得一票,这是正确的。鉴于 Enumerable.Range 返回一个 IEnumerable 并且似乎没有返回 ICollection,Enumerater.Range(a, b).ToList() 将始终过度分配。 A List 没有链接数组。当一个填满时,它会生成一个新的、更大的,然后将旧的留作垃圾收集,而不是缓冲区的链接列表(如果有人关心,这就是 StringBuider 所做的)。【参考方案4】:

我猜:

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

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

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

【讨论】:

以上是关于Enumerable.Range 的高内存消耗?的主要内容,如果未能解决你的问题,请参考以下文章

扩展 Enumerable.Range [重复]

为啥 Enumerable.Range 比直接 yield 循环快?

使用 Enumerable.Range() 填充字典的问题

关于 Enumerable.Range 与传统 for 循环的 foreach 的思考

从 Enumerable.Range 或 List<int> 填充 List<dynamic>

csharp 对于那些需要在一段时间内创建DropDownList的时候,您可以通过“Enumerable.Range”轻松完成。样本c