【问题标题】:Why does a List use almost 3x the memory of an array?为什么 List 使用的内存几乎是数组的 3 倍?
【发布时间】:2022-01-04 11:40:51
【问题描述】:

我正在尝试将大量数据添加到列表中,但它似乎比数组使用更多的 RAM。我想知道为什么会这样,是否有更好的解决方案。

这个带有数组的解决方案需要大约 78 MB 的 RAM。有意义,因为 4 字节 * 20000000 ~= 76 MB:

float[] arrayValues = new float[20000000];

for (int i = 0; i < 20000000; i++)
     arrayValues[i] = i;

但是这个带有列表的解决方案需要 206 MB (!!):

List<float> listValues = new();

for (int i = 0; i < 20000000; i++)
     listValues.Add(i);

怎么可能?它基本上是在做同样的事情——保存 20000000 个浮点值。额外的 128 MB 来自哪里?有没有更好的方法不会产生这么多开销?

【问题讨论】:

  • 你如何测量列表的内存使用情况?随着列表的动态增长,您的列表将需要多次调整大小 - 您是否有可能在测量中包含那些已被丢弃(但可能尚未被垃圾收集)的旧的、太小的数组?您是否尝试过为列表提供初始容量,即new(20000000)
  • 提示var listValues = new List&lt;float&gt;(20000000)
  • @Zelos a List use almost 3x the memory of an array? 它没有List 使用数组。您的代码虽然会导致内部数组的大量重新分配
  • 作为一个球场,List&lt;T&gt; 从一个大小为 4 的数组开始,每次空间不足时将其加倍,因此对于 20000000 个元素,它会将其内部数组的大小调整大约 23 次。所以周围有 22 个左右的旧数组,大小从 4 字节到 16.8M 不等,可能还没有被回收。给列表一个初始容量可以避免所有这些,它会在开始时分配一个正确大小的数组。
  • @Someprogrammerdude 自己看:source.dot.net/#System.Private.CoreLib/List.cs,cf7f4095e4de7646,或者文档:docs.microsoft.com/en-us/dotnet/api/… -- "它通过使用一个数组来实现IList&lt;T&gt;泛型接口,该数组的大小根据需要动态增加.”。它是一个列表,但不是链表,如果您对此感到困惑的话。

标签: c# list memory


【解决方案1】:

当您将Add 新项目放入List&lt;T&gt; 时,它必须进行内存重新分配,以便为这些新项目提供足够的空间。 我们来看看过程:

  List<float> listValues = new();

  int capacity = listValues.Capacity;

  for (int i = 0; i < 20000000; i++) {
    listValues.Add(i);

    if (capacity != listValues.Capacity) {
      capacity = listValues.Capacity;

      Console.WriteLine(capacity);
    }
  }

结果:

4
8
16
32
64
128
256
512
1024
2048
4096
8192
16384
32768
65536
131072
262144
524288
1048576
2097152
4194304
8388608
16777216
33554432 // <- Finally, list allocates memory for 33554432 items

如您所见,现在 33554432 项目已分配,4 + 8 + 16 + ... + 16777216垃圾。在 worst 的情况下,我们有 33554432分配物品和33554432垃圾物品;总共33554432 + 33554432 = 67108864 ~ 3 * 20000000,你可以看到这个3因素

你能做什么?

指定Capacity以避免重新分配(典型解决方案):

  // We can avoid all this mess with reallocations
  // by specifing required capacity: 20000000 items in our case 
  List<float> listValues = new(20000000);

  for (int i = 0; i < 20000000; i++) {
    listValues.Add(i);
  }

测量前收集所有垃圾:

  // Business as usual
  List<float> listValues = new();

  for (int i = 0; i < 20000000; i++) {
    listValues.Add(i);
  }

  // Collect garbage to measure real List efficency:
  // List allocates 33554432 items vs. 20000000 in case of array 
  // About ~70% overhead 
  GC.Collect(2);

【讨论】:

  • 谢谢你的详细解释,我现在明白了。调用 GC.Collect() 确实将 RAM 使用量减少到 78 MB。但是我注意到之后再次调用它,RAM 使用情况并没有恢复到以前的状态。在填写列表之前,我是 8 MB,之后是 15 MB。它永远不会退缩,15 MB 似乎是新的基线。这是为什么呢?
  • @Zelos:额外的 15 MB 可以是一些 静态数据(例如,从加载的程序集)。您开始使用 List&lt;T&gt; - System.Collections.Generic 和 .net 加载的程序集,包括类型、静态字段、资源等。
  • 在分配对象时,它们可能会在内存中的不同位置结束。这取决于它们在内存中存在多长时间以及这些对象有多大。并且在创建许多对象时,内存可能会变得碎片化(例如,在存在一些可用内存的地方使用的总内存中有空洞)。最佳做法是尽可能少地进行分配,因此在您的情况下,最好预先分配一个大列表,以防您需要空间。
  • 请记住,.NET GC 是一个压缩垃圾收集器——碎片是在 GC 期间处理的。当然,除了 LOH,对象超过 85k。
猜你喜欢
  • 1970-01-01
  • 2020-07-19
  • 2015-04-05
  • 1970-01-01
  • 2014-10-05
  • 2020-10-01
  • 2015-02-22
  • 2012-12-23
  • 1970-01-01
相关资源
最近更新 更多