Array.Count()比List.Count()慢得多
使用IEnumerable
Count()
的扩展方法时,数组至少比列表慢两倍。
Function Count() List 2,299 int[] 6,903
差异来自哪里?
我知道两者都在调用ICollection
的Count
属性:
如果源的类型实现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
只返回大小。
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
的实际实现的副本,并更改了原始测试程序以使用它,因此人们可以在调试器中单步执行它。
如果您运行以下代码的发布版本,您将获得与OP类似的时间。
对于List
和int[]
,分配给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.Count
和Array.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
快10倍。