【问题标题】:Looking for a faster implementation for IEnumerable/IEnumerator为 IEnumerable/IEnumerator 寻找更快的实现
【发布时间】:2010-12-17 18:13:24
【问题描述】:

我正在尝试优化一个并发集合,以尽量减少读取的锁争用。第一遍是使用链表,它允许我只锁定写入,而许多同时读取可以继续畅通无阻。这使用了自定义 IEnumerator产生下一个链接值。一旦我开始将集合上的迭代与普通的 List<T> 进行比较,我注意到我的实现速度大约是原来的一半(对于 1*m* 项目的集合上的 from x in c select x,我得到了 24ms for @ 987654324@ 和 49ms 用于我的收藏)。

所以我想我会使用ReaderWriteLockSlim 并牺牲一点读取争用,这样我就可以使用List<T> 作为我的内部存储。由于我必须在迭代开始时捕获读锁并在完成时释放它,我首先为我的IEnumerable 做了一个屈服模式,foreaching 覆盖了内部List<T>。现在我只得到 66ms

我查看了 List 的实际作用,它使用了 T[] 的内部存储和自定义的 IEnumerator 来向前移动索引并返回当前索引值。现在,手动使用 T[] 作为存储意味着更多的维护工作,但是,我正在追逐微秒。

但是,即使模仿 IEnumerator 在数组上移动索引,我能做的最好的事情也只有 ~38ms。那么是什么给了List<T> 其秘诀,或者说什么是迭代器的更快实现?

更新:原来我的主要速度罪魁祸首是运行调试编译,而 List<T> 显然是一个发布编译。在发布时,我的实现仍然比List<T> 慢一点,尽管在单声道上它现在更快。

我从朋友那里得到的另一个建议是 BCL 更快,因为它在 GAC 中,因此可以由系统预编译。我必须在 GAC 中进行测试才能验证该理论。

【问题讨论】:

  • @Arne:我添加了一个您可能想尝试的编辑。事实上,如果您不需要检测并发修改,您应该能够击败 List<T> 的性能:)

标签: c# performance ienumerable yield ienumerator


【解决方案1】:

在每次迭代时获取和释放锁听起来是个坏主意——因为如果您在迭代列表时执行AddRemove,这将使迭代器无效。例如,List<T> 肯定不喜欢这样。

您的用例是否允许调用者在他们的整个迭代过程中取出ReaderWriterLockSlim,而不是基于每个项目?这会更有效更健壮。如果没有,您打算如何处理并发问题?如果作者在我必须到达的位置之前添加了一个元素,那么一个简单的实现将返回相同的元素两次。删除会发生相反的情况 - 迭代器会跳过一个元素。

最后,.NET 4.0 是一种选择吗?我知道那里有一些高度优化的并发集合......

编辑:我不太确定您目前在手动构建迭代器方面的情况如何,但您可能想要调查的一件事是使用IEnumerator<T> 的结构,并使您的集合明确声明它返回那个 - 这就是 List<T> 所做的。它确实意味着使用可变结构,这会让小猫在世界各地哭泣,但如果这对性能绝对至关重要,并且你认为你可以忍受这种恐惧,那么至少值得一试。

【讨论】:

  • Jon - 我更新了我的帖子以澄清我在迭代开始时获取锁并在迭代完成后释放它。集合的重点正是修改会使迭代器无效的问题,因此我的第一个读取器无锁实现,以及我目前对 ReaderWriterLockSlim 的考虑,以允许许多具有廉价锁定语义的并发读取器。 .NET 4.0 还不是一个选项,因为此代码需要在单声道上运行。
  • 反应式扩展团队将 .NET 4 的高性能集合移植到 .NET 3.5。来自 RX 团队博客的“System.Threading,.NET 4 到 .NET 3.5 SP1 的并行扩展的反向移植”:blogs.msdn.com/rxteam/archive/2009/11/17/release-notes.aspx
  • 尝试了 struct 与 class 并没有发现任何区别。然后我发现了最大的差异,即我的测试使用的是 Debug build (Doh)。几乎逐行复制 List 枚举器,现在我的性能降低了 20%。我猜 BCL 的构建比 VS Release 优化得更好。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多