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

Tim*_*ter 7 .net c# linq memory-management

本来我想知道是否ToList比使用的构造函数分配的内存List<T>这需要一个IEnumerable<T>(没有区别).

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

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

  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"
Run Code Online (Sandbox Code Playgroud)

Mar*_*ell 13

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

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

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

一个有趣的测试是:

var list = new List<int>(10000000);
list.AddRange(Enumerable.Range(1, 10000000));
Run Code Online (Sandbox Code Playgroud)

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

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