C# 具有可枚举范围的高内存消耗?
最初我想知道C# 具有可枚举范围的高内存消耗?,c#,.net,linq,memory-management,C#,.net,Linq,Memory Management,最初我想知道ToList是否比使用List的构造函数分配了更多的内存,该构造函数采用IEnumerable(没有区别) 出于测试目的,我使用Enumerable.Range创建了一个源数组,可以通过1.ToList和2.创建List的实例。。两者都在创建副本 这就是为什么我注意到以下两者在内存消耗方面存在巨大差异: 可枚举范围(10000000)或 Enumerable.Range(1100000).ToArray() 当我使用第一个并调用ToList时,生成的对象比数组(38,26MB/64M
ToList
是否比使用List
的构造函数分配了更多的内存,该构造函数采用IEnumerable
(没有区别)
出于测试目的,我使用Enumerable.Range
创建了一个源数组,可以通过1.ToList
和2.创建List
的实例。。两者都在创建副本
这就是为什么我注意到以下两者在内存消耗方面存在巨大差异:
可枚举范围(10000000)
或Enumerable.Range(1100000).ToArray()
ToList
时,生成的对象比数组(38,26MB/64MB)多消耗约60%的内存
Q:这是什么原因,或者我的推理错误在哪里
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,从现在起,容量将增加一倍 最有可能的情况是,如果您将项目数降低到7500,那么您将看到数组降低到32Mbs以下,IList大小为32Mbs 您可以告诉
IList
初始大小应该是多少,这就是为什么如果在构造时给它IEnumerable
,它不应该过度分配内存
[编辑]评论之后
在
Enumerable.Range(a,b)
的情况下,它只返回IEnumerable
,而不是ICollection
。为了不过度分配列表
,构造期间传递的项也必须是i集合
,这可能与添加到列表时用于调整备份缓冲区大小的加倍算法有关。当您作为数组进行分配时,该的长度是已知的,可以通过检查IList[]
和/或ICollection[]
来查询;因此,它可以第一次分配一个大小合适的数组,然后只对内容进行块复制
对于序列,这是不可能的(序列不会以任何可访问的方式暴露长度);因此,它必须退回到“继续填充缓冲区;如果已满,则加倍并复制”
显然,这需要大约两倍的内存
一个有趣的测试是:
var list = new List<int>(10000000);
list.AddRange(Enumerable.Range(1, 10000000));
var list=新列表(10000000);
list.AddRange(可枚举的范围(1100000));
这将在最初分配正确的大小,同时仍然使用序列
tl;博士当传递一个序列时,构造函数首先检查是否可以通过转换到一个已知的接口来获得长度。我猜:
仅创建IEnumerable,尚未创建项Enumerable.Range(1100000)
创建一个数组,使用内存存储数字Enumerable.Range(1100000).ToArray()
创建数字和其他数据来管理列表(部分之间的链接。列表可以更改其大小,并且需要以块的形式分配内存)Enumerable.Range(1100000).ToList()
- 这是因为在列表中创建后备数组时使用了加倍算法。IEnumerable没有Count属性,因此在调用ToList时,它无法将支持数组预分配为目标大小。事实上,在每次调用MoveNext时,您都在调用列表中相应的Add
但是,Array.ToList可以覆盖基本ToList行为,以将列表初始化为正确的容量。此外,它可能是它的构造函数中的列表,它试图向下转换它对已知集合类型(如IList、ICollection、Array等)的IEnumerable引用
更新
事实上,它是在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);
}
}
公共列表(IEnumerable集合)
{
if(集合==null)
ThrowHelper.ThrowArgumentNullException(argument.collection除外);
ICollection collection1=作为ICollection的集合;
if(collection1!=null)
{
int count=collection1.count;
如果(计数=0)
{
这._items=列表._emptyArray;
}
其他的
{
此._items=新的T[计数];
collection1.CopyTo(此._项,0);
这个。_size=count;
}
}
其他的
{
这个。_size=0;
这._items=列表._emptyArray;
foreach(集合中的目标)
本条增补(obj);
}
}
这就是为什么如果在构造时给它IEnumerable,它就不会过度分配内存。
这是不正确的。只有当IEnumerable
也是一个ICollection
@Marc应该得到一个向上投票时,它才不会过度分配,这是正确的。给定的Enumerable.Range返回一个IEnumerable并且似乎不返回ICollection,Enumerar.Range(a,b).ToList()将始终过度分配。a列表
没有链接数组。当一个缓冲区被填满时,它会生成一个新的、更大的缓冲区,然后旧的缓冲区被留给垃圾收集,而不是缓冲区的链接列表(如果有人关心的话,StringBuider
就是这样做的)。仅供参考,您也可以调用list.trimOverse()代码>在第5行,而不是将列表初始化为精确的大小。@Marc:是的,但是您需要首先知道它在这里可能有用。正如Marc Gravell所指出的,另一种方法是使用range.Count()
初始化列表,然后使用AddRange(range)
。