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

66
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();

当我从stopwatch1stopwatch2中获取4个结果时,stopwatch1的值总是小于stopwatch2的。这意味着addrangeforeach快,有人知道为什么吗?


1
如果您担心GC敏感的性能问题(即针对移动设备的Unity游戏),AddRange将ToArray()传入的集合,这是一种分配。增加容量并进行手动添加可能会更快。 - Warty
在.NET Core中(不确定是从哪个版本开始),AddRange甚至更快,分配的内存更少。我不知道其中的诀窍,但CPU和内存性能的差异是实质性的。 - nawfal
9个回答

102

可能,AddRange 可以检查传递给它的值是否实现了 IListIList<T> 接口。如果是,它可以找出范围中有多少个值,从而知道需要分配多少空间... 而 foreach 循环可能需要重新分配多次。

此外,即使分配完毕,List<T> 也可以使用 IList<T>.CopyTo 将批量复制到基础数组中(对于实现 IList<T> 的范围,当然是这样)。

我猜您会发现,如果您尝试使用 Enumerable.Range(0, 100000) 替换 fillData 中的 List<T>,则两者需要的时间大约相同。


69
如果您正在使用Add,它会根据需要逐渐调整内部数组的大小(翻倍),从默认的起始大小10(如果我没记错)开始。如果您使用:
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 ^^^

21

由于AddRange会检查添加的项的大小并仅一次增加内部数组的大小。


9
反射器中 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()函数。


5
使用 AddRange 方法时,集合可以一次性增加数组的大小,并将值复制到其中。
使用 foreach 语句时,集合需要多次增加大小。
增加大小意味着需要复制完整的数组,这需要时间。

4

这就像是让服务员给你端来10次一杯的啤酒,或是让他一次给你送来10杯。

你认为哪个更快呢 :)


2
我认为这是内存分配优化的结果。对于AddRange,内存只会分配一次,而在每次遍历时都会进行重新分配。此外,AddRange实现中可能还有一些优化(例如memcpy)。

1

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

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

0

这是因为Foreach循环会逐个添加循环获取的所有值,而AddRange()方法会将获取的所有值作为“块”收集起来,并一次性将该块添加到指定位置。

简单地理解,就像你要从市场带回10件物品,一个一个地带回来还是一次性全部带回来更快。


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