飞道的博客

【Unity】 HTFramework框架(四十)Debug的性能监控

349人阅读  评论(0)

更新日期:2021年4月22日。
Github源码:[点我获取源码]
Gitee源码:[点我获取源码]

C#代码性能监控

C#代码运行时的性能消耗主要体现在两个方面,1是时间消耗,2是空间消耗

  • 时间消耗我们可以理解为某一段代码执行所消耗的CPU时间;
  • 空间消耗也即是消耗的内存空间,在C#中,内存空间分为栈空间堆空间,当然堆空间也叫做托管堆空间,因为堆这块已经托管给了CLR,他会负责开辟堆空间清理堆空间,所以我们主要关心的就是开辟清理这两个过程,因为在C#中这两个过程就是造成性能瓶颈的根本原因,也就是所谓的GC。

使用

Debug的性能监控模式

监控代码片段

一、我们可以使用如下方式监控一个代码片段的执行性能:

    void Start()
    {
   
        List<int> ints = new List<int>(1000000);

        //开始监控
        Main.m_Debug.BeginMonitor("计算一百万次");

        int test = 0;
        foreach (var i in ints)
        {
   
            test += i;
        }

        //结束监控
        MonitorData data = Main.m_Debug.EndMonitor();

        //打印监控日志
        data.ToString().Info();
    }

运行结果如下:

注意:
1.产生的堆内存垃圾:也即是在托管堆上开辟的托管空间;
2.触发GC次数:当CLR清理托管堆时也即是一次GC;
如果你的代码开辟的空间越多,或者导致的GC次数越多,则证明他们产生性能瓶颈的可能性越大。

这里虽然foreach遍历执行了一百万次整型加法运算,但并未产生堆内存垃圾。

二、然后我们改一下代码,将变量ints的赋值语句也放在监控代码段内:

    void Start()
    {
   
        //开始监控
        Main.m_Debug.BeginMonitor("计算一百万次");

        List<int> ints = new List<int>(1000000);

        int test = 0;
        foreach (var i in ints)
        {
   
            test += i;
        }

        //结束监控
        MonitorData data = Main.m_Debug.EndMonitor();

        //打印监控日志
        data.ToString().Info();
    }

运行结果如下:

这里由于List是引用类型,new一个引用类型会在托管堆上开辟新的空间,也即是生成了一段内存垃圾(虽然他暂时还不是内存垃圾,但用完之后就是了),可见,一个整型的大小是4字节,一百万个整型正好是4百万字节,正好是4M字节!不过4M的垃圾并没有被CLR看在眼里,触发GC次数为0证明了他没有因为这点新增的垃圾就去启动回收操作。

三、然后我们再改一下代码,手动GC一次:

    void Start()
    {
   
        //开始监控
        Main.m_Debug.BeginMonitor("计算一百万次");

        List<int> ints = new List<int>(1000000);

        int test = 0;
        foreach (var i in ints)
        {
   
            test += i;
        }

        //清理一次内存,将主动触发一次GC
        Main.m_Resource.ClearMemory();

        //结束监控
        MonitorData data = Main.m_Debug.EndMonitor();

        //打印监控日志
        data.ToString().Info();
    }

运行结果如下:

可以看到已经GC了一次,也即是回收了一次垃圾,但新增的堆内存垃圾仍然还有3M,很显然ints所指向的内存空间并没有被回收,CLR只是从其他地方收回了1M的空闲内存,因为CLR目前并不知道ints所指向的空间已经变成了垃圾,因为整个Start方法还没有结束,你还可以在后续调用ints,所以他还不是垃圾空间。


四、然后我们再改一下代码,将代码片段放在一个方法里面:

    void Start()
    {
   
        //开始监控
        Main.m_Debug.BeginMonitor("计算一百万次");

        Test();

        //清理一次内存,将主动触发一次GC
        Main.m_Resource.ClearMemory();

        //结束监控
        MonitorData data = Main.m_Debug.EndMonitor();

        //打印监控日志
        data.ToString().Info();
    }

    private void Test()
    {
   
        List<int> ints = new List<int>(1000000);

        int test = 0;
        foreach (var i in ints)
        {
   
            test += i;
        }
    }

运行结果如下:

可以看到,GC了一次后,ints所生成的4M垃圾空间已经被回收掉了,为什么这样就能回收掉呢?因为,ints的作用域变为了Test方法,在GC回收时,Test已经执行完毕,CLR收到明确指令:ints指向的已经是一块垃圾空间,可以回收。

注意:我们对比一下代码的时间消耗,未触发GC的执行时间为:0.0005秒,触发一次GC的执行时间为:0.0166秒,很明显,这多出来的近30倍执行时间,便是GC带来的性能损耗,这就好比使用时间的代价换来了空间!

终上所述,我们监控一个代码片段的执行效率时,重点关心的就是他的执行时间和产生了多少的堆内存垃圾,当然,如果在监控过程中触发了GC,那么最后产生的堆内存垃圾数量就不一定准确了,因为可能有一些垃圾已经被回收了,只不过,如果你的代码总是频繁的在触发GC,那么你一定得考虑重构他们了!

监控方法

当然,Debug也支持在监控模式中运行某一个方法,如下:

    void Start()
    {
   
        //监控模式执行Test
        MonitorData data = Main.m_Debug.MonitorExecute(Test);
        
        //打印监控日志
        data.ToString().Info();
    }

    private void Test()
    {
   
        List<int> ints = new List<int>(1000000);

        int test = 0;
        foreach (var i in ints)
        {
   
            test += i;
        }
    }

适用场景

一、我们可以使用性能监控器监测一些带来性能瓶颈的行为,比如,如下这个极具性能损耗的行为:

    void Start()
    {
   
        //开始监控
        MonitorData data = Main.m_Debug.MonitorExecute(Test, "字符串累加");
        
        //打印监控日志
        data.ToString().Info();
    }

    private void Test()
    {
   
    	//string累加一万次
        string test = "";
        for (int i = 0; i < 10000; i++)
        {
   
            test += "string";
        }
    }

运行结果如下:

此时产生的堆内存垃圾记录已然不准确了,因为已经触发了79次GC!


二、发现这个极大问题之后,我们即刻选择使用StringBuilder改进:

    void Start()
    {
   
        //开始监控
        MonitorData data = Main.m_Debug.MonitorExecute(Test, "StringBuilder累加");
        
        //打印监控日志
        data.ToString().Info();
    }

    private void Test()
    {
   
    	//string累加一万次
        StringBuilder builder = new StringBuilder();
        string test = "";
        for (int i = 0; i < 10000; i++)
        {
   
            builder.Append("string");
        }
        test = builder.ToString();
    }

运行结果如下:

可以看到惊人的优化效果,只产生了262KB的垃圾,并且避免了触发GC!

三、当然,我们也可以尝试使用string.Format来达到同样的效果:

    void Start()
    {
   
        //开始监控
        MonitorData data = Main.m_Debug.MonitorExecute(Test, "Format累加");
        
        //打印监控日志
        data.ToString().Info();
    }

    private void Test()
    {
   
        //同样是string累加一万次
        string test = "";
        for (int i = 0; i < 10000; i += 10)
        {
   
            test = string.Format("{0}{1}{1}{1}{1}{1}{1}{1}{1}{1}{1}", test, "string");
        }
    }

运行结果如下:

可以看到,string.Format虽然并不是最优手段,但他也同样能够带来一些优化效果。

总结,当你有一段比较复杂的靠人眼难以看出优劣的代码时,检测一下他运行时所消耗的时间和空间,是一个不错的优化指南!


转载:https://blog.csdn.net/qq992817263/article/details/116025230
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场