C# 如何为 List<T> 动态分配内存?

Posted

技术标签:

【中文标题】C# 如何为 List<T> 动态分配内存?【英文标题】:How does C# dynamically allocate memory for a List<T>? 【发布时间】:2018-12-16 01:47:16 【问题描述】:

来自LukeH's 回复what is the max limit of data into list<string> in c#?

理论上,当前 List 实现中可以存储的最大元素数是 Int32.MaxValue - 刚刚超过 20 亿。

我们看到一个 List 可以携带大量的项目。我假设编译器不仅为T 的每个新实现释放了 20 亿倍大小的空间List&lt;T&gt;,那么列表如何动态增长?它是否有指向内存中不连续空间的指针?

【问题讨论】:

@CamiloTerevinto 谢谢Camilo,我不知道为什么我总是想着看源头。 这可能是一个基本问题,但它不是引用的问题。 Memory allocation for collections in .NET 谈论堆栈与堆,并没有提及动态增长。 这个问题没有错,你得到了很好的答案,但我仍然想指出,原则上我们不知道,因为它是一个实现细节。在不同的平台和版本中可能会有所不同。 【参考方案1】:

List&lt;T&gt; 类被实现为在后台使用内部 T[] 数组。如果您使用List&lt;T&gt;(int) 构造函数对其进行初始化,它将分配一个指定大小的数组。如果您使用默认构造函数,它将使用默认容量 4,但在这种情况下,数组只会在第一次添加时分配。

每次向列表中添加元素时,它会首先检查是否已达到容量(即现有的Count是否等于Capacity)。如果是这样,它将创建一个大小是前一个数组两倍的新数组,将所有现有元素复制到其中,然后继续写入新元素。这将在后续添加元素时无限期地发生,直到达到您引用的硬限制 (Int32.MaxValue)。

在性能方面,这意味着添加元素是 O(1) 或 O(n) 操作,具体取决于是否需要增加容量(如 Add 中所述)。但是,由于容量在需要增加时会加倍,因此随着列表变大,这种重新分配的频率会呈指数下降。例如,从 4 开始,容量增加将发生在 4、8、16、32、64、128,... 元素。因此,调用Add n 次时重新分配的总成本大约为 4+8+16+…+n/8+n/4+n/2,仍然对应于 O(n)。

这是一个示例,显示了内部数组的状态以及一系列加法操作:

                               //   ┌┐
var list = new List<char>();   //   ││   Count:    0
                               //   └┘   Capacity: 0
                               //   ┌───┬───┬───┬───┐
list.Add('h');                 //   │ h │ ░ │ ░ │ ░ │   Count:    1
                               //   └───┴───┴───┴───┘   Capacity: 4
                               //   ┌───┬───┬───┬───┐
list.Add('e');                 //   │ h │ e │ ░ │ ░ │   Count:    2
                               //   └───┴───┴───┴───┘   Capacity: 4
                               //   ┌───┬───┬───┬───┐
list.Add('l');                 //   │ h │ e │ l │ ░ │   Count:    3
                               //   └───┴───┴───┴───┘   Capacity: 4
                               //   ┌───┬───┬───┬───┐
list.Add('l');                 //   │ h │ e │ l │ l │   Count:    4
                               //   └───┴───┴───┴───┘   Capacity: 4
                               //   ┌───┬───┬───┬───┬───┬───┬───┬───┐
list.Add('o');                 //   │ h │ e │ l │ l │ o │ ░ │ ░ │ ░ │   Count:    5
                               //   └───┴───┴───┴───┴───┴───┴───┴───┘   Capacity: 8
                               //   ┌───┬───┬───┬───┬───┬───┬───┬───┐
list.Add(' ');                 //   │ h │ e │ l │ l │ o │   │ ░ │ ░ │   Count:    6
                               //   └───┴───┴───┴───┴───┴───┴───┴───┘   Capacity: 8
                               //   ┌───┬───┬───┬───┬───┬───┬───┬───┐
list.Add('w');                 //   │ h │ e │ l │ l │ o │   │ w │ ░ │   Count:    7
                               //   └───┴───┴───┴───┴───┴───┴───┴───┘   Capacity: 8
                               //   ┌───┬───┬───┬───┬───┬───┬───┬───┐
list.Add('o');                 //   │ h │ e │ l │ l │ o │   │ w │ o │   Count:    8
                               //   └───┴───┴───┴───┴───┴───┴───┴───┘   Capacity: 8
                               //   ┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┐
list.Add('r');                 //   │ h │ e │ l │ l │ o │   │ w │ o │ r │ ░ │ ░ │ ░ │ ░ │ ░ │ ░ │ ░ │   Count:    9
                               //   └───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┘   Capacity: 16
                               //   ┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┐
list.Add('l');                 //   │ h │ e │ l │ l │ o │   │ w │ o │ r │ ░ │ ░ │ ░ │ ░ │ ░ │ ░ │ ░ │   Count:    10
                               //   └───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┘   Capacity: 16
                               //   ┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┐
list.Add('d');                 //   │ h │ e │ l │ l │ o │   │ w │ o │ r │ l │ d │ ░ │ ░ │ ░ │ ░ │ ░ │   Count:    11
                               //   └───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┘   Capacity: 16

符号表示已分配但仍未使用的空间。这些数组位置将包含default value 用于T。对于char,这将是空字符\0。但是,消费者永远不会看到这些值。

通过AddRange将多个元素相加时,最多只进行一次重新分配。如果将之前的容量翻倍不足以容纳所有新元素,则内部数组会立即增加到新的计数。

与添加不同,删除元素不会自动缩小列表。但是,您可以通过调用 TrimExcess 手动实现这一点。

如comments 中所述,上述某些方面(例如默认初始容量为 4)是从 .NET Framework 4.7.2 的source code 派生的实现细节。然而,核心原则是根深蒂固的,不太可能在其他/未来的框架中改变。

【讨论】:

优秀。你能介绍一下这方面的书或文件吗?【参考方案2】:

你的假设是正确的,编译器没有分配任何东西。 List&lt;T&gt; 类内部使用一个数组来存储元素,它会在每次调用Add 时检查数组的大小是否足够,如您所见in the source code:

public void Add(T item) 
    if (_size == _items.Length) EnsureCapacity(_size + 1);
    _items[_size++] = item;
    _version++;


private void EnsureCapacity(int min) 
    if (_items.Length < min) 
        int newCapacity = _items.Length == 0? _defaultCapacity : _items.Length * 2;
        // Allow the list to grow to maximum possible capacity (~2G elements) before encountering overflow.
        // Note that this check works even when _items.Length overflowed thanks to the (uint) cast
        if ((uint)newCapacity > Array.MaxArrayLength) newCapacity = Array.MaxArrayLength;
        if (newCapacity < min) newCapacity = min;
        Capacity = newCapacity;
    

【讨论】:

【参考方案3】:

源代码将是明确的,如 @CamiloTerevinto's answer 中的具体实现方式,但文档也涵盖了这一点。

Remarks section of the List&lt;&gt; class 声明:

List 类是 ArrayList 类的通用等价物。它通过使用一个数组来实现IList泛型接口,该数组的大小可以根据需要动态增加。

Remarks section of the Capacity property 详述:

Capacity 是在需要调整大小之前 List 可以存储的元素数量,而 Count 是 List 中实际存在的元素数量。

Capacity 总是大于或等于 Count。如果在添加元素时 Count 超过了容量,则通过在复制旧元素和添加新元素之前自动重新分配内部数组来增加容量。

【讨论】:

以上是关于C# 如何为 List<T> 动态分配内存?的主要内容,如果未能解决你的问题,请参考以下文章

如何为绑定到 List<T> 的 dataGridView 设置 columnNames?

如何为 C# 字典中的多个键分配一个值?

如何为每个单元分配号码?

C#中如何用for循环遍历List<类>?

c#中的音量控制。如何为卷分配值? [复制]

如何为多维数组动态分配内存