我正在做其他实验,直到这种奇怪的行为引起了我的注意.
代码在x64版本中编译.
如果键入1,则第3次运行List方法的成本比前2个多40%.输出是
List costs 9312
List costs 9289
Array costs 12730
List costs 11950
Run Code Online (Sandbox Code Playgroud)
如果键入2,则第三次运行Array方法的时间比前两次多30%.输出是
Array costs 8082
Array costs 8086
List costs 11937
Array costs 12698
Run Code Online (Sandbox Code Playgroud)
你可以看到模式,完整的代码附在下面(只是编译和运行):{提供的代码是最小的运行测试.用于获得可靠结果的实际代码更复杂,我将方法包装好并在适当的预热后测试了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);
sw.Reset();
sw.Start();
test.ListSum();
sw.Stop();
Console.WriteLine("List costs {0}", sw.ElapsedMilliseconds);
}
else
{
sw.Start();
test.ArraySum();
sw.Stop();
Console.WriteLine("Array costs {0}", sw.ElapsedMilliseconds);
sw.Reset();
sw.Start();
test.ArraySum();
sw.Stop();
Console.WriteLine("Array 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);
}
Console.ReadKey();
}
}
Run Code Online (Sandbox Code Playgroud)
简短的回答:这是因为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;
}
Run Code Online (Sandbox Code Playgroud)
因此,所有实现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;
}
Run Code Online (Sandbox Code Playgroud)
现在你可以清楚地看到,该方法体包含三个对接口类型变量的方法调用:GetEnumerator()、MoveNext()和Current(虽然Current实际上是属性,而不是方法,从属性读取值只是调用相应的 getter 方法)。
GetEnumerator()通常会创建某个辅助类的新实例,该辅助类实现IEnumerator< T >,因此能够一一返回所有值。需要注意的是,对于int[]和List< int >,这两个类的GetEnumerator()返回的枚举数类型是不同的。如果参数source的类型为int[],则GetEnumerator()返回SZGenericArrayEnumerator< int >类型的实例,如果source的类型为List< int >,则返回List< int >+Enumerator< int >类型的实例。
另外两个方法(MoveNext()和Current)在紧密循环中重复调用,因此它们的速度对于整体性能至关重要。不幸的是,调用接口类型变量(例如IEnumerator< int >)上的方法并不像普通实例方法调用那么简单。CLR必须动态找出变量中对象的实际类型,然后找出哪个对象的方法实现了相应的接口方法。
CLR 试图通过一个小技巧来避免在每次调用时进行这种耗时的查找。当第一次调用特定方法(例如MoveNext() )时,CLR 会查找进行此调用的实例的实际类型(例如SZGenericArrayEnumerator< int >,如果您调用Sum在int[])并查找地址方法,实现该类型的相应方法(即方法SZGenericArrayEnumerator< int >.MoveNext()的地址)。然后它使用这些信息生成辅助调度方法,该方法简单地检查实际实例类型是否与第一次调用时相同(即SZGenericArrayEnumerator< int >),如果是,则直接跳转到之前找到的方法的地址。因此,在后续调用中,只要实例类型保持不变,就不会进行复杂的方法查找。但是,当调用不同类型的枚举器时(例如计算List< int >的和时的List< int >+Enumerator< int >),CLR 不再使用这种快速调度方法。相反,使用另一种(通用)且速度慢得多的调度方法。
所以只要仅对数组调用Sum() ,CLR 就会使用快速方法调度对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;
}
}
Run Code Online (Sandbox Code Playgroud)
或者甚至更好,完全避免使用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;
}
}
Run Code Online (Sandbox Code Playgroud)
以下是我电脑上的结果: