为什么比包含更慢?

我设计了以下测试:

var arrayLength=5000; object[] objArray=new object[arrayLength]; for(var x=0;x<arrayLength;x++) { objArray[x]=new object(); } objArray[4000]=null; const int TestSize=int.MaxValue; System.Diagnostics.Stopwatch v= new Stopwatch(); v.Start(); for(var x=0;x<10000;x++) { objArray.Contains(null); } v.Stop(); objArray.Contains(null).Dump(); v.Elapsed.ToString().Dump("Contains"); //Any == v.Reset(); v.Start(); for(var x=0;xo==null); } v.Stop(); objArray.Any(x=>x==null).Dump(); v.Elapsed.ToString().Dump("Any"); //Any Equals v.Reset(); v.Start(); for(var x=0;xobject.Equals( obj,null)); } v.Stop(); objArray.Any(obj=>object.Equals( obj,null)).Dump(); v.Elapsed.ToString().Dump("Any"); 

null不存在时的结果:

  • Contains False 00:00:00.0606484
  • Any == False 00:00:00.7532898
  • Any object.Equals False 00:00:00.8431783

当元素4000存在null时:

  • Contains True 00:00:00.0494515
  • Any == True 00:00:00.5929247
  • Any object.Equals True 00:00:00.6700742

当元素10出现null时:

  • Contains True 00:00:00.0038035
  • Any == True 00:00:00.0025687
  • Any True 00:00:00.0033769

因此,当物体靠近前方时, Any会稍微快一点; 当它在后面时,速度要慢得多。 为什么?

Any都必须为它检查的每个元素调用一个委托(一个额外的callvirt指令,它不太可能被JIT内联)。 Contains仅执行该检查。 这就是Any慢的原因。 我怀疑Any看起来比包含很短的元素看起来更快的事实是基准测试不能很容易地反映它,因为它们非常接近。 方法调用的设置时间是在这种情况下完成的大部分工作(而不是实际的搜索操作)。

 The anonymous method: --- C:\Users\Mehrdad\AppData\Local\Temporary Projects\ConsoleApplication1\Program.cs Console.WriteLine(s.Any(a => a == 1)); 00000000 xor eax,eax 00000002 cmp ecx,1 00000005 sete al 00000008 ret Relevant part of Enumerable.Any code: ... 00000051 mov edx,eax 00000053 mov rcx,qword ptr [rbx+8] 00000057 call qword ptr [rbx+18h] // calls the anonymous method above 0000005a movzx ecx,al 0000005d test ecx,ecx ... 

任何都比较慢,因为Contains是根据你正在使用的特定容器(Array / List / etc)定制的,所以它没有启动IEnumerable的开销,一直调用MoveNext(),等等。

但是,使用Any会使重构变得更容易,因为如果你更改了那个集合,你可以继续使用它,所以如果你通过一个分析器知道这是一个重要的代码,我真的只能将它改为Contains。 如果是这样,你应该最终使用更智能的数据结构,就像HashSet一样,因为Any和Contains都是O(n)。

正如其他人已经注意到的,Contains和Any方法都是Enumerable的扩展方法。 性能的巨大差异有几个原因:

首先,您为Any提供委托,必须为每个对象调用,而Contains方法不必。 委托调用与调用接口方法的速度一样快。 因此,Any更慢。

接下来,其他人似乎错过了一些东西,Contains扩展方法对实现ICollection的集合进行了性能优化。 因为object []实现了ICollection,所以扩展方法调用会导致对数组本身进行方法调用。 在内部,这个array.Contains方法使用一个简单的for循环迭代数组来比较值。 这意味着只需迭代一次数组即可完成数组边界检查。

因为Any方法必须调用您的委托,所以不能像使用Contains方法那样进行性能优化。 这意味着Any方法使用IEnumerable接口迭代集合,这导致接口调用+数组边界检查+对每个元素的委托调用。 将它与array.Contains进行比较,其中没有接口调用,没有委托调用和单个边界检查。

[更新]:最后一点。 使用小集合的Any更快(在你的情况下在集合的开头有一个空值)的原因与Enumerable.Contains所做的ICollection的强制转换有关。当你自己进行ICollection演员时,你’我会看到对Contains的调用比Any更快:

 for(var x=0;x<10000;x++) { ICollection col = objArray; col.Contains(null); } 

我猜它与Any事件有关,Any是一个扩展方法,是LINQ库的一部分,涉及使用委托(通过Func <>语法)。 任何时候你必须呼吁一个单独的方法(特别是作为一个代表)它会减慢。