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;博士当传递一个序列时,构造函数首先检查是否可以通过转换到一个已知的接口来获得长度。

    我猜:

    • Enumerable.Range(1100000)
      仅创建IEnumerable,尚未创建项

    • 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)