C# - List<T> 的初始容量有何用途?

6
我将尝试完成以下操作:
int count = 50;
List<float> myList = new List<float>(50);
output[0] = 0.0f;

但是我遇到了错误:
ArgumentOutOfRangeException: Argument is out of range.
Parameter name: index

显然我误解了List(或者可能是任何其他集合?)的初始容量。请问有人能够解释一下初始容量的作用吗?


阅读有关 List<T>.Capacity 的内容。 - Habib
1
@rageit 默认的 size 是0。默认容量大概是16左右,我记得是这样的(根据我的记忆)。 - Timothy Shields
3
什么是“输出(output)”?你已经定义了一个大小为50的“myList”。 - John Koerner
@TimothyShields 感谢您的纠正。大多数框架的默认容量似乎为0 - https://dev59.com/u3I-5IYBdhLWcg3wn5y5 - rageit
@rageit 抛出了ArgumentOutOfRangeException异常,因为没有添加任何内容,尝试从索引0访问某些内容。容量与此无关。 - Trevor Tubbs
你还应该阅读这篇文章,它对预分配字典对象和不预分配的性能差异进行了基准测试。链接 - user3810900
5个回答

22
一个列表在底层有一个数组。这减少了内存重新分配的次数,当你添加50个元素时。这需要时间和内存,并给垃圾回收器一些工作要做。
这就是为什么 List(T).Capacity 是一个重要的东西。
这里有一些基准测试,测试100个.Add: (链接)
 Method A: Dictionary, no capacity
 Time:     1350 ms

Method B: Dictionary, has capacity
Time:     700 ms

Method C: Dictionary, const capacity
Time:     760 ms

Method D: Dictionary, over-large capacity
Time:     1005 ms

Method E: List, no capacity
Time:     1010 ms

Method F: List, accurate capacity
Time:     575 ms

因此,当您添加100个元素时,预分配似乎可以将所需时间减少一半。虽然我不喜欢过早优化,但如果您知道列表的大小,向CLR提供提示确实值得一试。


+1 有基准测试的好例子 - rageit
感谢简洁明了的解释! - Vesuvian

6

首先,为什么会出现异常:

通过定义初始容量,您只是指定了列表在需要调整大小之前可以存储的元素数量。

这并不意味着您在列表中有可访问的索引。您的列表仍然为空,因此当您执行以下操作时:

myList[0] = 0.0f;

您遇到了异常,但如果按照以下步骤操作:

List<float> myList = new List<float>(50);
myList.Add(0);
myList[0] = 2.0f;

现在,您不会因为索引0处存在一个元素而收到异常。

现在回答您问题的第二部分,“Capacity”实际上是什么意思:

请参见:List<T>.Capacity属性:

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

容量始终大于或等于计数。如果在添加元素时计数超过了容量,则通过自动重新分配内部数组来增加容量,然后复制旧元素并添加新元素。


5
列表是动态的。它可以在运行时动态添加和删除项目。
列表实现使用底层数组来存储列表项。
底层数组的长度被称为列表的容量
列表中的每个项目都存储在底层数组中。
当尝试向列表添加新项目且底层数组中没有更多的空间时,将创建一个新的、更大的数组。
旧数组中的所有项目将移动到新数组中,新数组还包含更多的空间用于新项目。
然后,新数组将被设置为列表的底层数组(旧数组将被处理)。

分配新数组,然后将项目从旧数组移动到新数组的操作是昂贵的(性能方面)。

因此,如果您从一开始就知道要向列表中添加多少项。则可以使用具有所需初始容量的基础数组创建列表。
这样,在运行时,列表分配新的基础数组的可能性更小。
您可以通过调用List(T) 构造函数(int32)来设置基础数组的初始长度
您可以通过调用List(T).Capacity 属性来获取当前数组的长度。

2
列表仍然为空,即没有元素,但内部已为50个项目保留了内存。这是一种优化,因为当您向列表添加项目时,它必须分配两倍于原有大小的新数组,然后将项目从旧数组复制到新数组中。
请注意,在此过渡期间,例如从256个项目添加一个项目,它在复制到新数组时总共分配了(256 + 512)768个项目的内存。本质上是前一容量的三倍。根据数组的大小,这可能导致内存不足异常。因此,如果您事先知道只会向列表添加257个项目,则可以事先使用257的容量。这将避免此三倍的内存使用,因为数组不必增长到足够大,因为它已经足够大了。请注意,当分配非常大的数组时,内存堆未压缩,因此碎片可能导致难以找到整个数组的连续块的情况,从而增加了内存不足异常发生的机会。当然,如果可能的话,您可能希望避免使用如此大的列表。
总之,如果您事先知道要添加多少项,或者可以估计(可能会留出一些余地),请使用容量。

0

它用于预分配列表所使用的内部数组。(此内部数组的大小由List.Capacity确定。)

然而,仅仅因为该内部数组的大小是固定的,并不意味着列表的元素就可用;在你(例如)使用List.Add()添加适当的元素之前,它们并不可用。列表当前可寻址的大小由List.Count指定。

当你向列表中添加新元素时,它首先检查内部数组是否有足够的空间,如果有,则增加一个内部计数器,并使用内部数组的一个位置。

如果没有足够的空间,它会分配一个新的缓冲区,大小是旧缓冲区的两倍,将所有元素从旧缓冲区复制到新缓冲区,然后丢弃旧缓冲区。然后它可以使用新缓冲区的一个位置。这显然是一个非常昂贵的操作。

通过设置初始大小,您可以避免一些(或全部)缓冲区重新分配。

一个常见的用法是当您知道列表的最大可能大小,但不知道最小大小,并且希望在进行尽可能少的重新分配的同时填充列表。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接