【问题标题】:Why is AddRange faster than using a foreach loop?为什么 AddRange 比使用 foreach 循环更快?
【发布时间】:2012-03-23 09:08:39
【问题描述】:
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 快。 有谁知道为什么?

【问题讨论】:

  • 如果您担心对 GC 敏感的性能(即在针对移动设备的 Unity 游戏中),AddRange 将 ToArray() 传入的集合,这是一个分配。提升容量和手动添加可能会更快。

标签: c# .net c#-4.0


【解决方案1】:

AddRange 可能会检查传递给它的值在哪里实现 IListIList&lt;T&gt;。如果是这样,它可以找出范围内有多少值,因此需要分配多少空间......而foreach 循环可能需要重新分配几次。

此外,即使在分配之后,List&lt;T&gt; 也可以使用IList&lt;T&gt;.CopyTo 将大容量复制到底层数组中(当然,对于实现IList&lt;T&gt; 的范围。)

我怀疑您会发现,如果您再次尝试测试但使用Enumerable.Range(0, 100000) 代替fillData 而不是List&lt;T&gt;,这两者将花费大约相同的时间。

【讨论】:

    【解决方案2】:

    如果您使用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 ^^^
    

    【讨论】:

      【解决方案3】:

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

      【讨论】:

        【解决方案4】:

        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 时,Collection 可以增加一次数组的大小,然后将值复制到其中。

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

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

          【讨论】:

            【解决方案6】:

            这就像让服务员给你送了十杯啤酒,然后让他一次给你送了 10 杯啤酒。

            你认为哪个更快:)

            【讨论】:

              【解决方案7】:

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

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

              【讨论】:

                【解决方案8】:

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

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

                【讨论】:

                  【解决方案9】:

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

                  简单的理解,这就像你有一个从市场上拿来的 10 件物品的清单,这会更快地把所有的东西一件一件或一次全部拿来。

                  【讨论】:

                    【解决方案10】:

                    AddRange 扩展不会遍历每个项目,而是将每个项目作为一个整体应用。 foreach 和 .AddRange 都有一个目的。 Addrange 将在您目前的情况下赢得速度的较量。

                    【讨论】:

                      猜你喜欢
                      • 2015-11-27
                      • 2010-11-11
                      • 1970-01-01
                      • 1970-01-01
                      • 2016-05-07
                      • 1970-01-01
                      • 2021-10-01
                      • 2022-11-17
                      相关资源
                      最近更新 更多