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

col*_*ang 10 c# performance

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

代码在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)

Sco*_*ain 6

存在的问题可以为您提供人为的答案.

应该在编写代码之后而不是之前完成优化.以最容易理解和维护的方式编写解决方案.然后,如果程序对于您的用例来说不够快,那么您使用分析工具并返回并查看实际瓶颈的位置,而不是您"认为"它的位置.

人们在你的情况下尝试做的大多数优化是花费6个小时来做​​一些会使运行时间缩短1秒的事情.大多数小型程序运行时间不足以抵消您尝试"优化"它所花费的成本.


据说这是一个奇怪的边缘情况.我修改了一下并通过分析器运行它,但我需要降级我的VS2010安装,以便我可以让.NET框架源退一步.


我使用更大的例子运行了探查器,我找不到为什么需要更长时间的理由.


Ňuf*_*Ňuf 1

简短的回答:这是因为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)

以下是我电脑上的结果:

  • 您的原始程序:9844、9841、12545、14384
  • 更快的求和扩展: 6149、6445、754、6145
  • EvenFasterSum 扩展: 1557、1561、553、1574