为什么 AddRange 比使用 foreach 循环更快

本文关键字:循环 foreach AddRange 为什么 | 更新日期: 2023-09-27 18:28:41

var fillData = new List<int>();
for (var i = 0; i < 100000; i++)
     fillData.Add(i);
var stopwatch1 = new Stopwatch();
stopwatch1.Start();
var autoFill = new List<int>();
autoFill.AddRange(fillData);
stopwatch1.Stop();
var stopwatch2 = new Stopwatch();
stopwatch2.Start();
var manualFill = new List<int>();
foreach (var i in fillData)
    manualFill.Add(i);
stopwatch2.Stop();

当我从stopwach1stopwach2中获取 4 个结果时,stopwatch1的值总是低于 stopwatch2 。这意味着addrange总是比foreach快。有谁知道为什么?

为什么 AddRange 比使用 foreach 循环更快

AddRange 潜在地,可以检查传递给它的值在哪里实现IListIList<T>。如果是这样,它可以找出该范围内有多少个值,从而需要分配多少空间......而foreach循环可能需要多次重新分配。

此外,即使在分配后,List<T>也可以使用 IList<T>.CopyTo 对底层数组执行大容量复制(当然,对于实现IList<T>的范围(。

我怀疑你会发现,如果你再次尝试测试,但使用Enumerable.Range(0, 100000)而不是List<T> fillData,两者将花费大约相同的时间。

如果您

使用的是 Add ,它会根据需要逐渐调整内部数组的大小(加倍(,从默认的起始大小 10 (IIRC(。如果您使用:

var manualFill = new List<int>(fillData.Count);

我希望它会发生根本性的变化(不再调整大小/数据复制(。

从反射器来看,AddRange内部做到这一点,而不是加倍增长:

ICollection<T> is2 = collection as ICollection<T>;
if (is2 != null)
{
    int count = is2.Count;
    if (count > 0)
    {
        this.EnsureCapacity(this._size + count);
        // ^^^ this the key bit, and prevents slow growth when possible ^^^

因为AddRange只检查添加项目的大小并增加内部数组的大小一次。

List AddRange 方法的反射器反汇编具有以下代码

ICollection<T> is2 = collection as ICollection<T>;
if (is2 != null)
{
    int count = is2.Count;
    if (count > 0)
    {
        this.EnsureCapacity(this._size + count);
        if (index < this._size)
        {
            Array.Copy(this._items, index, this._items, index + count, this._size - index);
        }
        if (this == is2)
        {
            Array.Copy(this._items, 0, this._items, index, index);
            Array.Copy(this._items, (int) (index + count), this._items, (int) (index * 2), (int) (this._size - index));
        }
        else
        {
            T[] array = new T[count];
            is2.CopyTo(array, 0);
            array.CopyTo(this._items, index);
        }
        this._size += count;
    }
}

如您所见,有一些优化,例如EnsureCapacity((调用和使用Array.Copy((。

使用 AddRange 集合可以增加数组的大小一次,然后将值复制到其中。

使用 foreach 语句,集合需要多次增加集合的大小。

增加大小意味着复制整个数组,这需要时间。

这就像让服务员给你端十次啤酒,让他一次给你端10瓶啤酒。

你认为什么更快:)

我想这是优化内存分配的结果。对于 AddRange 内存仅分配一次,而 foreach 在每次迭代时都会完成重新分配。

AddRange 实现中也可能有一些优化(例如 memcpy(

在手动添加项目之前尝试初始化初始列表容量:

var manualFill = new List<int>(fillData.Count); 

这是因为 Foreach 循环会将循环一次获得的所有值相加,并且
AddRange(( 方法将收集它获得的所有值作为"块",并立即将该块添加到指定位置。

简单地理解,这就像您有一个从市场上带来的 10 件物品的清单,这将更快地逐个或一次全部带来。