代码是在x64版本中编译的.
List costs 9312 List costs 9289 Array costs 12730 List costs 11950
如果键入2,则Array方法的第3次运行比第一次输出的时间多30%
Array costs 8082 Array costs 8086 List costs 11937 Array costs 12698
您可以看到模式,完整的代码附加如下(只需编译并运行):
{提供的代码是最小的运行测试.用于获得可靠结果的实际代码更复杂,我包装了方法,并在适当的加热后测试了100次}
class ListArrayLoop { readonly int[] myArray; readonly List<int> myList; readonly int totalSessions; public ListArrayLoop(int loopRange,int totalSessions) { myArray = new int[loopRange]; for (int i = 0; i < myArray.Length; i++) { myArray[i] = i; } myList = myArray.ToList(); this.totalSessions = totalSessions; } public void ArraySum() { var pool = myArray; long sum = 0; for (int j = 0; j < totalSessions; j++) { sum += pool.Sum(); } } public void ListSum() { var pool = myList; long sum = 0; for (int j = 0; j < totalSessions; j++) { sum += pool.Sum(); } } } class Program { static void Main(string[] args) { Stopwatch sw = new Stopwatch(); ListArrayLoop test = new ListArrayLoop(10000,100000); string input = Console.ReadLine(); if (input == "1") { sw.Start(); test.ListSum(); sw.Stop(); Console.WriteLine("List costs {0}",sw.ElapsedMilliseconds); sw.Reset(); sw.Start(); test.ListSum(); sw.Stop(); Console.WriteLine("List costs {0}",sw.ElapsedMilliseconds); sw.Reset(); sw.Start(); test.ArraySum(); sw.Stop(); Console.WriteLine("Array costs {0}",sw.ElapsedMilliseconds); } else { sw.Start(); test.ArraySum(); sw.Stop(); Console.WriteLine("Array costs {0}",sw.ElapsedMilliseconds); } Console.ReadKey(); } }
解决方法
长答案:
首先,看一下方法System.Linq.Enumerable.Sum()的声明(我省略了源参数的有效性检查,因为这不重要):
public static int Sum(this IEnumerable<int> source) { int num = 0; foreach (int num2 in source) num += num2; return num; }
所以实现IEnumerable< int >的所有类型都可以称之为扩展方法,包括int []和List& int>.关键字foreach是通过IEnumerable获取枚举器的缩写,T> .GetEnumerator()并遍历所有值.所以这个方法其实是这样做的:
public static int Sum(this IEnumerable<int> source) { int num = 0; IEnumerator<int> Enumerator = source.GetEnumerator(); while(Enumerator.MoveNext()) num += Enumerator.Current; return num; }
现在您可以清楚地看到,该方法体包含三个接口类型变量的方法调用:GetEnumerator(),MoveNext()和Current(虽然Current实际上是属性,而不是方法,从属性中读取值只是调用相应的getter方法).
GetEnumerator()通常创建一些辅助类的新实例,它实现IEnumerator& T>因此能够逐个返回所有值.重要的是要注意,在int []和List< int>,这两个类的GetEnumerator()返回的枚举类型不同.如果参数源的类型为int [],则GetEnumerator()返回SZGenericArrayEnumerator类型的实例,int>并且如果源的类型为List< int>,则返回类型为List< int>枚举< int>.
另外两种方法(MoveNext()和Current)在紧密循环中被重复调用,因此它们的速度对于整体性能至关重要.接口类型变量(如IEnumerator< int>)的Unfortunatelly调用方法并不像普通实例方法调用那么简单. CLR必须动态地找出变量中实际的对象类型,然后找出哪个对象的方法实现对应的接口方法.
CLR试图避免在每次调用时都花费时间查找一些小技巧.当第一次调用特定方法(如MoveNext())时,CLR找到实际进行此调用的实例类型(例如SZGenericArrayEnumerator< int>,如果您在int []上调用Sum)并找到地址的方法,它为这种类型实现了相应的方法(即方法SZGenericArrayEnumerator< int> .MoveNext()的地址).然后它使用这些信息来生成辅助调度方法,它简单地检查实际实例类型是否与第一次调用相同(即SZGenericArrayEnumerator< int>),如果是,则直接跳转到方法的地址早.所以在随后的调用中,只要实例的类型保持不变,就不会复杂的方法查找.但是当调用不同类型的枚举器(例如在计算List< int>的总和的情况下为List< int>枚举器< int>)时,CLR不再使用该快速调度方法.相反,使用了另一种(通用)和更慢的调度方法.
所以只要在数组上调用Sum(),CLR就使用fast方法调度GetEnumerator(),MoveNext()和Current.当列表中调用Sum()时,CLR切换到较慢的调度方式,因此性能下降.
如果您关心性能,请为您要调用Sum()的每种类型实现您自己的单独Sum()扩展方法.这样可以确保CLR使用快速调度方式.例如:
public static class FasterSumExtensions { public static int Sum(this int[] source) { int num = 0; foreach (int num2 in source) num += num2; return num; } public static int Sum(this List<int> source) { int num = 0; foreach(int num2 in source) num += num2; return num; } }
或者甚至更好,避免使用IEnumerable< T>接口(因为它仍然引起明显的开销).例如:
public static class EvenFasterSumExtensions { public static int Sum(this int[] source) { int num = 0; for(int i = 0; i < source.Length; i++) num += source[i]; return num; } public static int Sum(this List<int> source) { int num = 0; for(int i = 0; i < source.Count; i++) num += source[i]; return num; } }
以下是我电脑的结果:
>您的原始程序:9844,9841,12545,14384> FasterSumExtensions:6149,6445,754,6145> EvenFasterSumExtensions:1557,1561,553,1574