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

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

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

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

  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" 

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

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

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

一个有趣的测试是:

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

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

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

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

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

更新

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

 public List(IEnumerable collection) { if (collection == null) ThrowHelper.ThrowArgumentNullException(ExceptionArgument.collection); ICollection collection1 = collection as ICollection; if (collection1 != null) { int count = collection1.Count; if (count == 0) { this._items = List._emptyArray; } else { this._items = new T[count]; collection1.CopyTo(this._items, 0); this._size = count; } } else { this._size = 0; this._items = List._emptyArray; foreach (T obj in collection) this.Add(obj); } } 

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

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

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

[编辑]评论后

Enumerable.Range(a, b)的情况下,它仅返回IEnumerable而不是ICollection 。 对于List ,不要过度定位构造期间传递的项目,也必须是ICollection

我猜可能是:

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

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

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