加入收藏 | 设为首页 | 会员中心 | 我要投稿 李大同 (https://www.lidatong.com.cn/)- 科技、建站、经验、云计算、5G、大数据,站长网!
当前位置: 首页 > 百科 > 正文

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

发布时间:2020-12-15 06:57:50 所属栏目:百科 来源:网络整理
导读:我正在做其他的实验,直到这个奇怪的行为引起了我的注意. 代码是在x64版本中编译的. 如果键入1,列表方法的第3次运行比第一次输出的时间多40% List costs 9312List costs 9289Array costs 12730List costs 11950 如果键入2,则Array方法的第3次运行比第一次输
我正在做其他的实验,直到这个奇怪的行为引起了我的注意.

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

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

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();
    }
}

解决方法

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

(编辑:李大同)

【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!

    推荐文章
      热点阅读