【问题标题】:Why is this function faster and why are multiple enumerations of it faster than the first?为什么这个函数更快,为什么它的多个枚举比第一个更快?
【发布时间】:2014-05-19 21:13:54
【问题描述】:

我需要一个 TakeLast<T>(int n) 风格的 LINQ 函数。我遇到了这个 StackOverflow 帖子:https://stackoverflow.com/a/3453282/825011。我喜欢这个答案只是因为它是一个简单的实现。然后,我的另一位同事指出Reverse() 肯定比Skip(length - n) 更昂贵。这导致我写了一个测试。

这是相互竞争的功能。

public static IEnumerable<T> TakeLast<T>( this IEnumerable<T> c, int n ) {
    return c.Reverse().Take( n ).Reverse();
}


public static IEnumerable<T> TakeLast2<T>( this IEnumerable<T> c, int n ) {
    var count = c.Count();
    return c.Skip( count - n );
}

我定时获取枚举Enumerable.Range( 0, 100000 )的最后10个元素的执行。我发现:

  1. TakeLast() 快 5 倍左右。
  2. 第一次枚举后,TakeLast() 的枚举速度明显加快。

这是我的代码的 .NET Fiddle(最初在本地运行,但也在这里演示。):http://dotnetfiddle.net/ru7PZE

问题

  1. 为什么TakeLast() 更快?
  2. 为什么TakeLast() 的第二个和第三个枚举比第一个快,但TakeLast2() 的所有枚举都差不多?

【问题讨论】:

  • 因为它已被枚举并且似乎在调用 Reverse 时它正在得到优化。
  • 这可能与Enumerable.Range的实现密切相关。如果你真的关心List&lt;T&gt; 的性能,你应该用它来描述。如果您真的关心Range 的性能,您可以使用正确的开始/计数值创建一个新的Range
  • 您的基准测试存在缺陷...
  • @LB2 是的,但基准测试存在缺陷。第二个热切地进行第一次迭代,第二个懒惰地进行。第一种方法懒惰地完成整个操作。由于他在打印结果之前记录了时间,因此延迟的所有内容均不计入基准。
  • @verdesrobert 您的基准实际上并未执行查询,只是构建查询。输入实现IList 的事实只是意味着Count 不需要迭代序列,它可以免费获得计数,从而消除了生成查询的时间差异。它还允许Skip 直接跳到正确的位置。实现IList的源序列是第二种方法的最佳情况;第一种方法有好处,但没有那么多

标签: c# .net performance linq


【解决方案1】:

直到您打印出秒表的经过时间之后,您才实现查询结果。 LINQ 查询使用延迟执行来避免在枚举之前实际执行查询。对于第二种方法,您在构建查询之前调用CountCount 需要实际枚举整个结果集来计算它的值。这意味着您的第二种方法每次都需要迭代序列,而第一个查询能够成功地将其工作推迟到您显示时间之后。我希望它有更多的工作要做,在许多情况下,它只是成功地等到你完成计时。

至于为什么第一个在多次调用时更快,这几乎可以归结为执行任何代码时发生的 JIT 预热这一事实。第二种方法确实获得了加速,但由于它不会每次都省略查询的迭代(这是其成本的很大一部分),因此加速百分比要小得多。

请注意,您的第二个查询将源序列迭代两次(除非枚举恰好实现了ICollection)。这意味着如果对象是可以高效迭代多次的东西,那不是问题。如果它实现了IList,它实际上会快得多,但如果它类似于IQueryable,每次迭代时都需要对数据库执行昂贵的查询,它需要这样做两次,而不是一次。如果它是一个在多次迭代时甚至没有相同内容的查询,那么这可能会导致所有排序问题。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-06-07
    • 1970-01-01
    • 1970-01-01
    • 2014-03-06
    • 1970-01-01
    相关资源
    最近更新 更多