列出<t> .AddRange实现次优</t>

时间:2010-01-23 12:44:25

标签: c# .net performance list addrange

对我的C#应用​​程序进行概要分析表明在List<T>.AddRange中花费了大量时间。使用Reflector查看此方法中的代码表明它调用了List<T>.InsertRange,其实现方式如下:

public void InsertRange(int index, IEnumerable<T> collection)
{
    if (collection == null)
    {
        ThrowHelper.ThrowArgumentNullException(ExceptionArgument.collection);
    }
    if (index > this._size)
    {
        ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.index, ExceptionResource.ArgumentOutOfRange_Index);
    }
    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;
        }
    }
    else
    {
        using (IEnumerator<T> enumerator = collection.GetEnumerator())
        {
            while (enumerator.MoveNext())
            {
                this.Insert(index++, enumerator.Current);
            }
        }
    }
    this._version++;
}

private T[] _items;

可以说,界面的简单性(只有一个InsertRange的重载)证明了运行时类型切换和转换的性能开销。 但是,我用(*)指出的3行背后可能是什么原因? 我认为它可以改写为更快的替代方案:

is2.CopyTo(this._items, index);

你认为没有理由不使用这种更简单,更明显更快的选择吗?

修改

感谢您的回答。因此,一致意见认为,这是针对以缺陷/恶意方式实施CopyTo的输入集合的保护措施。对我来说,不断付出代价1)运行时类型检查2)临时数组的动态分配3)复制操作的两倍,当所有这些都可以通过定义2或更多的InsertRange重载来保存时,其中一个获得IEnumerable,第二个获得List<T>,第三个获得T[]。后两者本可以实现两倍于当前情况的速度。

编辑2:

我确实实现了一个与List相同的类FastList,除了它还提供了一个带有T []参数的AddRange重载。这种重载不需要动态类型验证和元素的双重复制。我通过向最初为emtpy的列表添加1000次4字节数组,对List.AddRange进行了FastList.AddRange的分析。我的实现比标准List.AddRange的速度快9倍(9!)。在我们的应用程序的一个重要使用场景中,List.AddRange大约占运行时的5%,用一个提供更快的AddRange的类替换List可以将应用程序运行时间提高4%。

3 个答案:

答案 0 :(得分:11)

它们阻止ICollection<T>的实现访问插入范围之外的目标列表的索引。如果调用IndexOutOfBoundsException的错误(或“操纵”)实现,则上面的实现会导致CopyTo

请注意,T[].CopyTo内部实现为memcpy,因此添加该行的性能开销很小。如果您为大量呼叫增加安全性的成本很低,那么您也可以这样做。

编辑:我觉得奇怪的是,在调用ICollection<T>.CopyTo之后,调用EnsureCapacity(复制到临时数组)不会立即发生。如果它被移动到该位置,那么跟随任何synchronous exception列表将保持不变。原样,只有当插入发生在列表的末尾时,该条件才成立。这里的理由是:

  • 在更改列表元素之前,所有必要的分配都会发生。
  • Array.Copy的调用不会失败,因为
    • 已分配内存
    • 已经检查了界限
    • 源和目标数组的元素类型匹配
    • 没有像C ++那样使用“复制构造函数” - 它只是一个memcpy
  • 可以抛出异常的唯一项是对ICollection.CopyTo的外部调用以及调整列表大小和分配临时数组所需的分配。如果在移动元素以进行插入之前发生了所有这三个,则更改列表的事务不会抛出同步异常。
  • 最后注意事项:此地址严格例外行为 - 上述理由 添加线程安全性。

编辑2(对OP编辑的回应):你有没有对此进行过分析?你正在大胆宣称微软应该选择一个更复杂的API,所以你应该确保你在当前方法很慢的断言中是正确的。我从来没有对InsertRange的性能有任何问题,我很确定通过重新设计算法而不是通过重新实现动态列表,可以更好地解决某人遇到的任何性能问题。所以你不要以负面的方式对待我,请记住以下几点:

  • 不希望无法阻止我的开发团队中的人reinvent the square wheel
  • 肯定希望我的团队中的人关注潜在的性能问题,并询问有关他们的代码可能产生的副作用的问题。这一点在出现时胜出 - 但只要人们提出问题,我就会驱使他们将问题转化为可靠的答案。如果你能告诉我一个应用程序通过最初看起来是一个坏主意获得了显着的优势,那么这就是事情的发展方式。

答案 1 :(得分:2)

这是一个很好的问题,我很难想出一个理由。参考源中没有任何提示。一种可能性是,当实现ICollection&lt;&gt; .CopyTo()方法的类反对复制到0以外的起始索引时,它们会尝试避免问题。或者作为安全措施,防止集合搞乱阵列它应该无法访问的元素。

另一个是当集合以线程不安全的方式使用时,这是一个反措施。如果一个项目被另一个线程添加到集合中,那么集合类'CopyTo()方法将失败,而不是Microsoft代码。合适的人将接到服务电话。

这些都不是很好的解释。

答案 2 :(得分:0)

如果您在一分钟内考虑它,您的解决方案就会出现问题,如果您以这种方式更改代码,那么您实际上是应该添加应该添加对内部数据结构的访问权限的集合。

这不是一个好主意,例如,如果List数据结构的作者计算出比数组更好的存储数据的底层结构,则无法更改List的实现,因为所有集合都期望数组进入CopyTo函数。

本质上,您将巩固List类的实现,即使面向对象编程告诉我们数据结构的内部实现应该是可以在不破坏其他代码的情况下进行更改的内容。