哪个更快:单个(谓词)或Where(谓词)。单个()

这个答案引起的讨论让我很好奇。 哪个更快:

someEnumerable.Single(predicate); 

要么

 someEnumerable.Where(predicate).Single(); 

毕竟,第一个更短,更简洁,似乎是专门建造的。

甚至ReSharper建议前者:

在此处输入图像描述

我在上一篇文章中争论说,它们在function上是相同的,并且应该具有非常相似的运行时。

LINQ到对象

没有什么像这样的基准回答像这样的问题:

(更新)

 class Program { const int N = 10000; volatile private static int s_val; static void DoTest(IEnumerable data, int[] selectors) { Stopwatch s; // Using .Single(predicate) s = Stopwatch.StartNew(); foreach (var t in selectors) { s_val = data.Single(x => x == t); } s.Stop(); Console.WriteLine(" {0} calls to Single(predicate) took {1} ms.", selectors.Length, s.ElapsedMilliseconds); // Using .Where(predicate).Single() s = Stopwatch.StartNew(); foreach (int t in selectors) { s_val = data.Where(x => x == t).Single(); } s.Stop(); Console.WriteLine(" {0} calls to Where(predicate).Single() took {1} ms.", selectors.Length, s.ElapsedMilliseconds); } public static void Main(string[] args) { var R = new Random(); var selectors = Enumerable.Range(0, N).Select(_ => R.Next(0, N)).ToArray(); Console.WriteLine("Using IEnumerable (Enumerable.Range())"); DoTest(Enumerable.Range(0, 10 * N), selectors); Console.WriteLine("Using int[]"); DoTest(Enumerable.Range(0, 10*N).ToArray(), selectors); Console.WriteLine("Using List"); DoTest(Enumerable.Range(0, 10 * N).ToList(), selectors); Console.ReadKey(); } } 

有点令人震惊的是, .Where(predicate).Single()获胜约两倍。 我甚至两次运行两个案例以确保缓存等不是一个因素。

 1) 10000 calls to Single(predicate) took 7938 ms. 1) 10000 calls to Where(predicate).Single() took 3795 ms. 2) 10000 calls to Single(predicate) took 8132 ms. 2) 10000 calls to Where(predicate).Single() took 4318 ms. 

更新结果:

 Using IEnumerable (Enumerable.Range()) 10000 calls to Single(predicate) took 7838 ms. 10000 calls to Where(predicate).Single() took 8104 ms. Using int[] 10000 calls to Single(predicate) took 8859 ms. 10000 calls to Where(predicate).Single() took 2970 ms. Using List 10000 calls to Single(predicate) took 9523 ms. 10000 calls to Where(predicate).Single() took 3781 ms. 

Where(predicate).Single()会比Singe(predicate)

编辑:你会期望以类似的方式编码Single()Single(predicate) ,但事实并非如此。 一旦找到另一个元素, Single()完成,但后者会找到所有令人满意的元素。

附加兴趣点(原始答案) – 对不同类型的集合类型进行特殊优化,而FirstSingleCount等其他方法不利用集合的类型。

所以Where(predicate).Single()能够做一些Single(predicate)没有的优化

基于Where(predicate).Single()Single(predicate)的实际实现,似乎前者实际上是懒惰的,而后者总是迭代整个IEnumerableSingle()既返回枚举的唯一元素,又测试枚举是否没有或具有多个值,这可以通过最多询问枚举的下两个元素来实现。 Single(predicate)当前是以一种方式实现的,它需要遍历整个枚举以确认谓词对于一个且仅一个元素是否为true ,因此性能(和function,见下文)差异。

虽然它们看起来在function上是相同的,但是有些情况下,不仅性能,而且实际function完全不同,即无限枚举,

 public IEnumerable InfiniteEnumeration() { while (true) { yield return 1; } } 

如果使用这两种方法运行此函数,则可以正确完成; 另一个……我们可能要等。

 var singleUsingWhere = InfiniteEnumeration().Where(value => value != 0).Single(); var singleUsingSingle = InfiniteEnumeration().Single(value => value != 0); 

奇怪的是,微软决定以这种方式实现Single(predicate) ……甚至Jon Skeet设法解决了这种疏忽 。

Linq for for objects Single存在设计缺陷,意味着:

  1. 这是毫无意义地保持匹配数量的计数,而不是找到匹配,然后如果找到另一个匹配则抛出。
  2. 即使在第三场比赛之后,它仍然一直持续到序列结束。
  3. 它可以抛出OverflowException ; 它不太可能,但它完全可以成为一个错误。

https://connect.microsoft.com/VisualStudio/feedback/details/810457/public-static-tsource-single-tsource-this-ienumerable-tsource-source-func-tsource-bool-predicate-doesnt-throw-immediately-上第二匹配结果#

这使得它在0或1匹配的情况下略微变慢(当然只有第二个是非错误情况),并且在多于1个匹配的情况下(错误情况)慢得多。

与其他Linq提供商一样,这取决于; 它往往大致相同,但是对于给定的提供者来说,完全有可能对一个或另一个提供者效率较低而另一个给定的提供者则相反。

[编辑:.NET Core不再是这种情况,上面的描述不再适用。 这使得单个调用.Single(pred)效率比.Where(pred).Single()

我认为这是苹果与橘子的情况。

我们必须考虑Single(predicate)的当前实现与以下实现的不同之处:

 public static TSource Single(this IEnumerable source, Func predicate) { return Where(source, predicate).Single(); } 

Where的实现返回一个Enumerable.Iterator ,它看起来像是在识别在不同线程中的同一个迭代器上MoveNext时发生的竞争条件。

来自ILSpy:

 switch (this.state) { case 1: this.enumerator = this.source.GetEnumerator(); this.state = 2; break; case 2: break; default: return false; } 

Single(predicate)First(predicate)的当前实现不处理这种情况。

我很难想到这在现实场景中意味着什么 ,但我猜测“bug”还没有修复,因为在某些multithreading场景中行为会被改变。