为什么这个函数更快,为什么它的多个枚举比第一个更快?

时间:2014-04-07 14:40:41

标签: c# .net performance linq

我需要一个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()更快~5x。
  2. 第一次枚举后,TakeLast()的枚举速度明显加快。
  3. 这是我的代码的.NET小提琴(最初在本地运行,但也在这里演示。):http://dotnetfiddle.net/ru7PZE

    问题

    1. 为什么TakeLast()更快?
    2. 为什么TakeLast()的第二和第三个枚举比第一个更快,但TakeLast2()的所有枚举大致相同?

1 个答案:

答案 0 :(得分:10)

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

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

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