为什么 LINQ .Where(predicate).First() 比 .First(predicate) 更快?

81

我正在进行性能测试并发现像这样的一个 LINQ 表达式:

result = list.First(f => f.Id == i).Property

比......慢

result = list.Where(f => f.Id == i).First().Property

这似乎有些反直觉。我本以为第一个表达式会更快,因为在谓词满足时可以立即停止迭代整个列表,而我本以为使用 .Where() 表达式可能需要在调用结果子集的 .First() 之前遍历整个列表。即使后者采用了短路计算,它也不应该比直接使用 First 更快,但实际上却是这样。

以下是两个非常简单的单元测试,说明了这一点。 在 .Net 和 Silverlight 4 上启用优化编译时,TestWhereAndFirst 比 TestFirstOnly 快约 30%。 我已经尝试让谓词返回更多结果,但性能差异相同。

有人能解释一下为什么 .First(fn).Where(fn).First() 更慢吗?我发现 .Count(fn).Where(fn).Count() 相比也有类似的反直觉结果。

private const int Range = 50000;

private class Simple
{
   public int Id { get; set; }
   public int Value { get; set; }
}

[TestMethod()]
public void TestFirstOnly()
{
   List<Simple> list = new List<Simple>(Range);
   for (int i = Range - 1; i >= 0; --i)
   {
      list.Add(new Simple { Id = i, Value = 10 });
   }

   int result = 0;
   for (int i = 0; i < Range; ++i)
   {
      result += list.First(f => f.Id == i).Value;
   }

   Assert.IsTrue(result > 0);
}

[TestMethod()]
public void TestWhereAndFirst()
{
   List<Simple> list = new List<Simple>(Range);
   for (int i = Range - 1; i >= 0; --i)
   {
      list.Add(new Simple { Id = i, Value = 10 });
   }

   int result = 0;
   for (int i = 0; i < Range; ++i)
   {
      result += list.Where(f => f.Id == i).First().Value;
   }

   Assert.IsTrue(result > 0);
}

6
你的初始想法是错误的:LINQ采用延迟计算,因此当调用First()时,它将查询(Where(...)的返回值)仅匹配一次并永远不会再次查询。因此,检查的元素数量与直接使用谓词调用First(...)时完全相同。 - Jon
2
我得到了相同的结果,.Where().First() 是 0.021 秒,而 .First() 是 0.037 秒。这是使用一个简单的 int 列表。 - Ry-
5
这是一个证明,可以在 IdeOne 上运行比我的电脑更快。 - Ry-
5
你没有在秒表上调用 Reset() 方法;你的测试实际上显示了 First() 要快得多。 - Jay
亲自看看,First 比 Where().First() 更快,真正的答案是 Where 可枚举在每个线程中都被缓存,因此它似乎比直接使用旧式 List/Array 迭代器的 First 更快。https://dotnetfiddle.net/OrUUSG - Akash Kava
显示剩余6条评论
1个回答

54

我得到了相同的结果:where + first比first更快。

正如Jon所指出的那样,Linq使用惰性评估,因此两种方法的性能应该(并且是)在很大程度上相似。

查看反编译器,First使用简单的foreach循环遍历集合,而Where具有专门针对不同集合类型(数组、列表等)的各种迭代器。这可能就是给Where带来微小优势的原因。


11
如果我是一个框架开发者,只是在内部实现First(fn)为return Where(fn).First(),那么它将与当前的First实现完全相同,只是速度更快!这似乎是微软犯下的一个糟糕的疏忽。 - dazza
5
也可以将 .Count(fn) 与 .Where(fn).Count() 进行比较。由于 .Count(fn) 使用 foreach,而 .Where(fn).Count() 使用专用迭代器,后者更快。使用像 .First(fn) 和 .Count(fn) 这样的便捷方法可以使代码更加简洁,因此似乎是正确的做法,但 .Where(fn).Method() 速度明显更快。Grrrr! - dazza
5
请查看 https://dotnetfiddle.net/k11nX6,你会惊讶地发现答案是错误的。 - Akash Kava
7
现在试试这个!!! https://dotnetfiddle.net/OrUUSG , 你的答案是错误的!!! 尝试任何组合,证明你的答案是正确的。真正的答案是 "Where enumerable caches Enumerable that is advantage over old List/Array iterator",但这与速度无关。 - Akash Kava
1
尝试使用@AkashKava上面的小提琴,在4.7.2中,它确实显示了一些测试的速度更快,但当您将其切换到使用.Net 5时,差异消失了,因此看起来微软已经纠正了这个故障。 - Mog0
显示剩余4条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接