C# · 12月 30, 2021

c# – 为什么在这个简单的测试中,方法的速度与触发顺序有关?

我正在做其他的实验,直到这个奇怪的行为引起了我的注意.

代码是在x64版本中编译的.

如果键入1,列表方法的第3次运行比第一次输出的时间多40%

List costs 9312List costs 9289Array costs 12730List costs 11950

如果键入2,则Array方法的第3次运行比第一次输出的时间多30%

Array costs 8082Array costs 8086List costs 11937Array 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(); }}解决方法 简短的答案:是因为 CRL已经针对调用接口类型的调度方法进行了优化.只要特定接口的方法调用在相同类型(实现此接口)上进行调用,CLR使用仅检查实例类型的快速调度例程(仅3个指令),并且匹配时,直接跳转到预先计算的特定地址方法.但是当相同的接口的方法调用在另一种类型的实例上时,CLR将调度到较慢的例程(可以为任何实际类型调度方法).

长答案:
首先,看一下方法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