List .AddRange实现次优

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

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

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

 is2.CopyTo(this._items, index); 

您是否认为没有使用这种更简单且明显更快的替代品的理由?

编辑:

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

编辑2:

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

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

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

编辑:我发现奇怪的部分是在调用ICollection.CopyTo不会立即调用ICollection.CopyTo (复制到临时数组)。 如果它被移动到该位置,则在任何同步exception之后 ,列表将保持不变。 原样,只有插入发生在列表末尾时,该条件才成立。 这里的推理是:

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

编辑2(对OP编辑的回应):你有没有对此进行分析? 您正在做出一些大胆的声明,即Microsoft应该选择更复杂的API,因此您应该确保在当前方法运行缓慢的断言中是正确的。 我从来没有遇到过InsertRange的性能问题,而且我很确定任何有人遇到的性能问题都可以通过重新设计算法来解决,而不是重新实现动态列表。 所以你不要以负面的方式对我采取严厉的态度,请记住以下几点:

  • 不想让我的开发团队的人们不喜欢重新发明方形轮 。
  • 绝对希望我的团队中的人关注潜在的性能问题,并询问他们的代码可能产生的副作用。 这一点在出现时胜出 – 但只要人们提出问题,我就会驱使他们将问题转化为可靠的答案。 如果你能告诉我一个应用程序通过最初看起来是一个坏主意获得了显着的优势,那么这就是事情的发展方式。

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

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

这些都不是很好的解释。

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

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

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