Array.Count()比List.Count()慢得多

使用IEnumerable Count()的扩展方法时,数组至少比列表慢两倍。

 Function Count() List 2,299 int[] 6,903 

差异来自哪里?

我知道两者都在调用ICollectionCount属性:

如果源的类型实现ICollection,则该实现用于获取元素的数量。 否则,此方法确定计数。

对于列表,它返回List.Count ,对于array, Array.Length 。 而且, Array.Length应该比List.Count

基准测试:

 class Program { public const long Iterations = (long)1e8; static void Main() { var list = new List(){1}; var array = new int[1]; array[0] = 1; var results = new Dictionary(); results.Add("List", Benchmark(list, Iterations)); results.Add("int[]", Benchmark(array, Iterations)); Console.WriteLine("Function".PadRight(30) + "Count()"); foreach (var result in results) { Console.WriteLine("{0}{1}", result.Key.PadRight(30), Math.Round(result.Value.TotalSeconds, 3)); } Console.ReadLine(); } public static TimeSpan Benchmark(IEnumerable source, long iterations) { var countWatch = new Stopwatch(); countWatch.Start(); for (long i = 0; i < iterations; i++) source.Count(); countWatch.Stop(); return countWatch.Elapsed; } } 

编辑:

leppie和Knaģis的答案非常棒,但我想补充一句话。
正如Jon Skeet所说:

实际上有两个等效的块,只是测试不同的集合接口类型,并使用它首先找到的任何一个(如果有的话)。 我不知道.NET实现是否首先测试ICollection或ICollection – 我可以通过实现两个接口来测试它,但当然可以从每个接口返回不同的计数,但这可能是过度的。 除了轻微的性能差异之外,对于性能良好的集合并不重要 – 我们希望首先测试“最可能的”接口,我认为这是通用接口。

通用的可能是最有可能发生的,但如果你反转这两个,即在通用的之前调用非generics强制转换,Array.Count()变得比List.Count()快一点。 另一方面,List的非通用版本较慢。

很高兴知道是否有人想在1e8迭代循环中调用Count()

 Function ICollection Cast ICollection Cast List 1,268 1,738 Array 5,925 1,683 

原因是Enumerable.Count()ICollection执行ICollection转换,以从列表和数组中检索计数。

使用此示例代码:

 public static int Count(IEnumerable source) { ICollection collection = source as ICollection; if (collection != null) { return 1; // collection.Count; } } 

你可以确定演员阵容需要花费更长的时间,实际上大部分时间用于计算:

 Function Count() List 1,575 int[] 5,069 

关键可能是文档中的这一陈述(强调我的):

在.NET Framework 2.0版中,Array类实现System.Collections.Generic.IList,System.Collections.Generic.ICollection和System.Collections.Generic.IEnumerable通用接口。 这些实现在运行时提供给数组 ,因此文档构建工具不可见。 因此,通用接口不会出现在Array类的声明语法中,并且没有可通过将数组转换为通用接口类型(显式接口实现)来访问的接口成员的参考主题。

32位分析分析(全部以ms为单位,仅有趣的位,JIT内联禁用):

 Name Count 'Inc Time' 'Ex Time' 'Avg Inc Time' 'Avg Ex Time' System.Linq.Enumerable::Count():int32  20000000 13338.38 7830.49 0.0007 0.0004 System.SZArrayHelper::get_Count():int32  10000000 4063.9 2651.44 0.0004 0.0003 System.Collections.Generic.List::get_Count():int32 10000000 1443.99 1443.99 0.0001 0.0001 System.Runtime.CompilerServices.JitHelpers::UnsafeCast(Object):System.__Canon  10000004 1412.46 1412.46 0.0001 0.0001 

System.SZArrayHelper::get_Count()似乎为数组大小写调用System.Runtime.CompilerServices.JitHelpers::UnsafeCast

对于列表, List.Count只返回大小。

Inc time是费用,包括儿童电话。 Ex time仅为方法体的成本。

禁用内联时, Array.Count()速度是缓慢的两倍。

这可能是因为提到现在已删除的答案。 看起来应用的属性( ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)SecuritySafeCritical )会阻止运行时内联调用,因此差异很大(在我的情况下,在32位模式下慢38倍)。

要自己描述一下:

获取https://github.com/leppie/IronScheme/raw/master/IronScheme/tools/IronScheme.Profiler.x86.dll运行应用程序(仅限x86 build):

 regsvr32 IronScheme.Profiler.x86.dll set COR_PROFILER={9E2B38F2-7355-4C61-A54F-434B7AC266C0} set COR_ENABLE_PROFILING=1 ConsoleApp1.exe 

当应用程序退出时,会创建一个report.tab文件,然后可以在Excel中使用该文件。

我发布这个,不是作为答案,而是提供一个更可测试的环境。

我已经获取了Enumerable.Count()的实际实现的副本,并更改​​了原始测试程序以使用它,因此人们可以在调试器中单步执行它。

如果您运行以下代码的发布版本,您将获得与OP类似的时间。

对于Listint[] ,分配给is2的第一个is2将为非null,因此将调用is2.Count

所以看起来差异来自.Count的内部实现。

 using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; namespace ConsoleApplication1 { class Program { public const long Iterations = (long)1e8; static void Main() { var list = new List() { 1 }; var array = new int[1]; array[0] = 1; var results = new Dictionary(); results.Add("int[]", Benchmark(array, Iterations)); results.Add("List", Benchmark(list, Iterations)); Console.WriteLine("Function".PadRight(30) + "Count()"); foreach (var result in results) { Console.WriteLine("{0}{1}", result.Key.PadRight(30), Math.Round(result.Value.TotalSeconds, 3)); } Console.ReadLine(); } public static TimeSpan Benchmark(IEnumerable source, long iterations) { var countWatch = new Stopwatch(); countWatch.Start(); for (long i = 0; i < iterations; i++) Count(source); countWatch.Stop(); return countWatch.Elapsed; } public static int Count(IEnumerable source) { ICollection is2 = source as ICollection; if (is2 != null) return is2.Count; // This is executed for int[] AND List. ICollection is3 = source as ICollection; if (is3 != null) return is3.Count; int num = 0; using (IEnumerator enumerator = source.GetEnumerator()) { while (enumerator.MoveNext()) num++; } return num; } } } 

鉴于此信息,我们可以简化测试,只关注List.CountArray.Count之间的时序差异:

 using System; using System.Collections.Generic; using System.Diagnostics; namespace ConsoleApplication1 { class Program { static void Main() { int dummy = 0; int count = 1000000000; var array = new int[1] as ICollection; var list = new List {0}; var sw = Stopwatch.StartNew(); for (int i = 0; i < count; ++i) dummy += array.Count; Console.WriteLine("Array elapsed = " + sw.Elapsed); dummy = 0; sw.Restart(); for (int i = 0; i < count; ++i) dummy += list.Count; Console.WriteLine("List elapsed = " + sw.Elapsed); Console.ReadKey(true); } } } 

上面的代码为调试器外部的发布版本运行提供了以下结果:

 Array elapsed = 00:00:02.9586515 List elapsed = 00:00:00.6098578 

在这一点上,我自己“当然可以优化Count()以识别T[]并直接返回.Length 。所以我改变了Count()的实现,如下所示:

 public static int Count(IEnumerable source) { var array = source as TSource[]; if (array != null) // Optimised for arrays. return array.Length; // This is executed for int[] ICollection is2 = source as ICollection; if (is2 != null) return is2.Count; // This is executed for List. ICollection is3 = source as ICollection; if (is3 != null) return is3.Count; int num = 0; using (IEnumerator enumerator = source.GetEnumerator()) { while (enumerator.MoveNext()) num++; } return num; } 

值得注意的是,即使在进行此更改之后,我的系统上的arrays仍然较慢,尽管非arrays必须进行额外的转换!

结果(发布版本)对我来说是:

 Function Count() List 1.753 int[] 2.304 

我完全不知道要解释这最后的结果......

这是因为int[]需要强制转换,而List则不需要。 如果你要使用Length属性,那么结果会大不相同 – 大约。 比List.Count()快10倍。