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<T>
初始大小应该是多少,这就是为什么如果您在构造时给它IEnumerable<T>
,它不应该过度分配内存。
在cmets之后[编辑]
对于Enumerable.Range(a, b)
,它只返回IEnumerable<T>
,而不是ICollection<T>
。对于List<T>
不过度分配在构造过程中传递的项目也必须是ICollection<T>
【讨论】:
which is why if you give it the IEnumerable<T> at construction time, it shouldn't over allocate memory.
这是不正确的。只有当IEnumerable<T>
也是ICollection<T>
时,它才不会过度分配
@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 比直接 yield 循环快?
关于 Enumerable.Range 与传统 for 循环的 foreach 的思考
从 Enumerable.Range 或 List<int> 填充 List<dynamic>
csharp 对于那些需要在一段时间内创建DropDownList的时候,您可以通过“Enumerable.Range”轻松完成。样本c