您当前的位置: 首页 >  Jave.Lin unity

Unity、C#.net 的部分 API GC 测试

Jave.Lin 发布时间:2020-12-06 22:07:33 ,浏览量:4

文章目录
  • 测试的 Unity 版本
  • FAQ-测试中常见的 GC 问题
  • Demo
    • Check_NewDotNetManagedObj
    • Check_GetComponentAndTryGetComponent
    • Check_GetComponentsInChildren
    • Check_ReturnRefOrValue
    • Check_ToString_Concat_Trim
    • Cehck_EnumToString
    • Check_StringEquals
    • Check_StringToArg
    • Check_NewString
    • Check_ToLowerString
    • Check_ReplaceString
    • Check_GameObject_get_name_or_get_tag
    • Check_GetTransform
    • Check_BoxingOrUnBoxing
    • Check_Enumerator
    • Check_EnumeratorGCSize
    • Check_Task_Delay_TimerMgr
    • Check_PassCallbackWhichHasGenericType
    • Check_List
    • Check_Using
    • Check_ReuseCoroutinue
    • Check_EnumGetValues
    • Check_Lambda
    • Check_LayerMaskGetMask
    • Check_ParamsToArg
    • Check_UGUI_TextToggleOrUpdate
    • Check_UGUI_ImageToggle
    • Check_UGUI_RawImageToggle
    • Check_MeshRenderToggle
  • 什么时候适合 删除缓存 并 GC.Collect?
  • Project
  • References
  • Incremental garbage collection

测试的 Unity 版本

Unity 2019 3.8f1

FAQ-测试中常见的 GC 问题
  • 什么叫 GC? GC 是 Garbage Collector 的缩写,意思是:垃圾收集器
  • 为何要有 GC? 因为以往我们在写C/C++ 等相对低级的语言中,程序员可以只对堆内存的分配和释放 但是由于内存对象的管理异常复杂,特别是业务逻辑繁杂的更加难以管理,时不时就会出现一个:“0xXXXXXX 内存不可访问”、“Out of Memory”,之类的错误提示
  • 所以在 C#.Net 中,CLR(Common Language Runtime)底层就封装了对托管的内存对象的管理,免得出现类似上面的错误提示
    • 上面说了有托管对象,那么对应的就有非托管对象不是 CLR 中管理的,这些对象需要手动释放
    • 另外,Unity 在 IL2CPP 后,会针对 GC 下的签名内容都替换成 Unity 内置的 C++ 版 GC,与 C#.NET 有一些差异,但是触发 GC.Collect 的时机都是差不多的
  • GC.Collect 什么时候触发? CLR 底层会有检测当前托管堆内存可用大小是否小于下限,当到达下限,就会触发 GC.Collect,也就是不断 GC.Alloc 分配了托管堆内存,而导致托管堆可用大小变小。CLR 库中的类对象基本都是托管的,所以我们在各种 new、CreateActivor() 之类的接口来创建对象时,底层都会调用 GC.Alloc
  • 为何要避免 GC(就上面说的GC.Collect)? 因为 GC 需要底层需要处理的消耗比较大,具体可自行百度,这个与 GC 的检测机制有关,Unity 还可以在 PlayerSetting 中设置 GC 模式,是:Generation 还是 Increatement 方式
  • 为何要控制 GC 频率? 上面说了,GC 会导致 CPU 消耗大,如果托管对象多而小,导致的一些内存碎片过细,会更加消耗 CPU 因此,我们尽可能的将托管对象缓存起来,反复使用,这样在运行过程中就可以减少的 GC.Alloc 操作
  • 如何实现 0 GC? 如果你想这么做,就不要用 Unity,用 UE(而且,我相信 UE 应该也是有封装类似的内存管理系统的,这样同样也会有 GC 问题,只不过 C++ 在内存操作方面可以比 C#.NET 更为灵活的底层 API,你可以跳过 GC 来自己分配内存),就是用那些没有内置:内存分配统计、内存自动回收,运行库的语言,因为 .net 的底层很多都有 GC 问题,特别是字符串处理,因此很难做到 0 GC

下面时自行测试的内容,想要了解 .net、Unity 中其他 API 是否有 GC,可以留言告知我一下,我会测试后,更新到 blog

下面的测试,都使用 Unity 的 Profiler

如果你想快速入门 Unity Profiler,其实可以自己打开一下这个 Profiler Window 就打开知道怎么用了 如果还是看不懂,就看官方教程也行(但是是英文的):Fixing Performance Problems - 2019.3

Demo Check_NewDotNetManagedObj
    public class DotNetManagedObj
    {
        public int a1, a2, a3, a4, a5, a6;
    }

    private DotNetManagedObj cached_obj;
    private void Check_NewDotNetManagedObj()
    {
        Profiler.BeginSample("Check_NewDotNetManagedObj");

        Profiler.BeginSample("1");
        new DotNetManagedObj();
        Profiler.EndSample();

        Profiler.BeginSample("2");
        if (cached_obj == null) cached_obj = new DotNetManagedObj();
        Profiler.EndSample();

        Profiler.EndSample();

        // Proflie 结果
        // 1 方式 直接 new .net 的托管对象,都会有 GC.Alloc,当不断的 GC.Alloc,就会让 ManagedHeap.UnusedSize 越来越小,小到一定程度,就会触发 GC.Collect 回收垃圾
        // 而 GC.Collect 是很耗時的,所以尽力避免不必要的 GC.Alloc
        // 2 方式是缓存了对象,因此只有第一次 new 有 Alloc,后续复用改对像就没有 Alloc 了
    }

在这里插入图片描述

Check_GetComponentAndTryGetComponent
    private void Check_GetComponentAndTryGetComponent()
    {
        Profiler.BeginSample("Check_GetComponentAndTryGetComponent");
        Profiler.BeginSample("1");
        {
            var checker = go.GetComponent();
        }
        Profiler.EndSample();

        Profiler.BeginSample("2");
        {
            go.TryGetComponent(out CheckGC checker);
        }
        Profiler.EndSample();
        Profiler.EndSample();

        // Profile 结果
        //  1 方式[没] GC
        //  2 方式[没] GC
        // 两种方式都没有,但是再以前有人发现 go.GetComponent 时,在 Editor 下才有 GC,真机上不会有
        //      具体可参考:https://zhuanlan.zhihu.com/p/26763624
        // 但现在我再 Editor 下测试也是没有的,有可能 Unity 做了优化,我的 Unity 版本是 2019.3.8f1
        // 但是 GetComponent 会消耗 CPU,因为原理上是 for 遍历 GameObject 下的所有 MonoBehaviour 组件
        // 建议尽可能将 GetComponent 的对象缓存起来,便于后续直接访问
    }

在这里插入图片描述

Check_GetComponentsInChildren
    private List mesh_render_list = new List();
    public static class ListPoolT
    {
        private static Stack pool = new Stack();
        public static List FromPool()
        {
            return pool.Count > 0 ? pool.Pop() : new List();
        }
        public static void ToPool(List list)
        {
            list.Clear();
            pool.Push(list);
        }
    }
    private void Check_GetComponentsInChildren()
    {
        Profiler.BeginSample("Check_GetComponentsInChildren");

        Profiler.BeginSample("1");
        foreach (var item in go.GetComponentsInChildren())
        {

        }
        Profiler.EndSample();

        Profiler.BeginSample("2");
        mesh_render_list.Clear();
        go.GetComponentsInChildren(false, mesh_render_list);
        foreach (var item in mesh_render_list)
        {

        }
        Profiler.EndSample();

        Profiler.BeginSample("3");
        var list = ListPoolT.FromPool();
        go.GetComponentsInChildren(false, list);
        foreach (var item in list)
        {

        }
        ListPoolT.ToPool(list);
        Profiler.EndSample();

        Profiler.EndSample();

        // Profile 结果
        //  1 方式[有] GC
        //  2 方式[没] GC
        //  3 方式[没] GC(本质上和 2 方式一样,只不过,这里我将一些简单的封装方式给大家参考)
    }

在这里插入图片描述

Check_ReturnRefOrValue
   private TestingCls ReturnCls()
    {
        return new TestingCls();    // 返回的是托管堆中的内存
    }
    private TestingStruct ReturnStruct()
    {
        return new TestingStruct(); // new 了一个执行栈上的内存
    }
    private void ReturnClsCached(TestingCls ret)
    {
        //ret.xx = xxx; // 使用的是缓存的对象来存放数据,避免重复的创建托管堆内存对象
    }
    private TestingCls testing_cls_cached = new TestingCls();
    private void Check_ReturnRefOrValue()
    {
        Profiler.BeginSample("Check_ReturnRefOrValue");


        Profiler.BeginSample("1");
        ReturnCls();
        Profiler.EndSample();


        Profiler.BeginSample("2");
        ReturnStruct();
        Profiler.EndSample();


        Profiler.BeginSample("3");
        ReturnClsCached(testing_cls_cached);
        Profiler.EndSample();


        Profiler.EndSample();

        // Profile 结果
        //  1 方式返回的是引用类型对象,创建对象的内存是在托管堆的,所以有 GC
        //  2 方式返回的是值类型的对象,创建对象的内存是再线程执行栈中的数据,再函数声明时入栈,返回时出栈,所以没有托管退管理,也就没有 GC
        //  3 方式重复利用缓存对象来作返回数据对象的载体,所以没有 GC
        //  其实你也可以理解执行栈也算是一个简单的 GC,只不过,这个 GC 系统的申请与回收的性能极高,因为管理方式很简单,Push, Pop 的方式
    }

在这里插入图片描述

Check_ToString_Concat_Trim
    private StringBuilder sb = new StringBuilder();
    private string testing_str = " abc ";
    private void Check_ToString_Concat_Trim()
    {
        Profiler.BeginSample("Check_ToString_Concat_Trim");

        Profiler.BeginSample("Check_ToString");

        {
            const int LOOP_MAX = 100;

            Profiler.BeginSample("1");
            {
                for (int i = 0; i  { }); // no gc,但是匿名函数,且非闭包就没有 GC
        Profiler.EndSample();

        Profiler.BeginSample("555.666");
        Action act = (MyType inst) => { }; // no gc,将匿名函数存于一个变量,也没有 GC
        TestDefaultParams4(act);
        Profiler.EndSample();

        Profiler.BeginSample("555.777");
        act = Testing; // have gc, 本质上和 555.444 是一样的,只不过尝试将一个预定义的方法指向一个临时的 act 方法变量,结果与 555.444 是一样有 GC 的
        TestDefaultParams4(act);
        Profiler.EndSample();

        Profiler.BeginSample("555.888");
        var temp_var = 1; // 让下面行数变成闭包:在匿名函数使用临时变量即可
        Action act1 = (MyType inst) => { temp_var++; }; // have gc,一旦函数变成闭包函数,就会有 GC,因此在频繁调用的地方尽量不使用闭包
        // 参考:unity 官方手册说明:https://docs.unity3d.com/cn/current/Manual/BestPracticeUnderstandingPerformanceInUnity4-1.html
        TestDefaultParams4(act); // have gc
        Profiler.EndSample();

        Profiler.BeginSample("666");
        AddTimer(timer_mgr_same_inst1.SameMethod); // have gc
        Profiler.EndSample();

        Profiler.BeginSample("777");
        AddTimer(Testing); // have gc
        Profiler.EndSample();

        Profiler.BeginSample("888");
        AddTimer1(Testing); // have gc
        Profiler.EndSample();


        Profiler.EndSample();

        // Profile 结果
        // 带有: 泛型参数的 callback 作为参数,都会有 GC
        // 带有: 泛型参数的匿名函数没有 GC
        // 闭包函数,都有 GC
        // (如果让一个匿名成为闭包,在匿名函数内容使用到不在闭包函数内的外部的临时变量即可,
        //   因为 C# 闭包原理是新建一个匿名类,将临时变量存于类成员中,
        //  这点与 IEnumerator + yield 的方式很类似,都是语法糖)

        // 在函数的方法参数传参时:

        /*
        以下说明参考:unity 官方手册说明:https://docs.unity3d.com/cn/current/Manual/BestPracticeUnderstandingPerformanceInUnity4-1.html

        IL2CPP 下的匿名方法
        目前,通过查看 IL2CPP 所生成的代码得知,对System.Function 类型变量的声明和赋值将会分配一个新对象。无论变量是显式的(在方法/类中声明)还是隐式的(声明为另一个方法的参数),都是如此。
        因此,使用 IL2CPP 脚本后端下的匿名方法必定会分配托管内存。在 Mono 脚本后端下则不是这种情况。
        此外,由于方法参数的声明方式不同,将导致IL2CPP 显示出托管内存分配量产生巨大差异。正如预期的那样,闭包的每次调用会消耗最多的内存。
        预定义的方法在 IL2CPP 脚本后端下作为参数传递时,其__分配的内存几乎与闭包一样多__,但这不是很直观。匿名方法在堆上生成最少量的临时垃圾(一个或多个数量级)。
        因此,如果打算在 IL2CPP 脚本后端上发布项目,有三个主要建议:
        - 最好选择不需要将方法作为参数传递的编码风格。
        - 当不可避免时,最好选择匿名方法而不是预定义方法。
        - 无论脚本后端为何,都要避免使用闭包。
         * */

    }

在这里插入图片描述

Check_List
    private int[] arr4linq = { 3, 2, 9 };
    private List _check_list = new List();
    private void Check_List()
    {
        Profiler.BeginSample("Check_List");

        if (_check_list.Count == 0) _check_list.AddRange(arr4linq); // 内部:List Capacity 不足时会有 GC.Alloc,所以说,如果外部很多 List 需要临时使用的,都建议使用对象池,减少不必要的 GC.Alloc

        Profiler.BeginSample("1");
        var tolist = _check_list.ToArray(); // 内部 new T[Count],有 GC
        Profiler.EndSample();

        Profiler.BeginSample("2");
        _check_list.Sort(); // 内部有 IComparer 实现对象的 new ,有 GC
        Profiler.EndSample();

        Profiler.BeginSample("3");
        _check_list.Reverse(); // 内部反转索引内容,无 GC
        Profiler.EndSample();

        Profiler.BeginSample("4");
        _check_list.GetRange(0, 1); // 内部 new List,有 GC
        Profiler.EndSample();

        Profiler.BeginSample("5");
        _check_list.GetEnumerator(); // 内部 new Enumerator,但是 Enumerator 是内部的 struct 结构体,所以返回是存于执行栈帧的数据中,所以无 GC
        Profiler.EndSample();

        Profiler.BeginSample("6");
        _check_list.FindAll(v => v > 0); // 内部 new List,有 GC
        Profiler.EndSample();

        Profiler.BeginSample("7");
        _check_list.Capacity = 10; // 当 capcity 不够指定大小时,内部 new T[],然后 Array.Copy _items 到 new T[] 中,有 GC.Alloc
        Profiler.EndSample();

        Profiler.BeginSample("8");
        _check_list.ConvertAll(v => v as object ); // 内部 new List,有 GC
        Profiler.EndSample();

        Profiler.BeginSample("9");
        _check_list.AsReadOnly(); // 内部 new ReadOnlyCollection,有 GC
        Profiler.EndSample();

        Profiler.EndSample();

        // Profile 结果
        // 只测试了部分的 API,但是,其实 List(准确的说,.net 中的 API)大多都有 GC 问题,在排查 GC 问题,建议使用 ILSpy 或是 VS 自带的反编译来查看源码功能
        // 确定有 GC 后,建议使用缓存方式来避免重复,无意义的 GC.Alloc 而导致 GC.Collect
    }

在这里插入图片描述

Check_Using
    public class TestingCanDispose : IDisposable
    {
        public void Dispose()
        {
            GC.SuppressFinalize(this); // 不调用 ~XXX 析构
        }
    }
    private void Check_Using()
    {
        Profiler.BeginSample("CheckUsing");
        using (var obj = new TestingCanDispose()) // using 自动释放只不过时自动调用实现了:IDisposable 的接口,所以 GC 还时肯定有的
        {

        }
        Profiler.EndSample();
    }

在这里插入图片描述

Check_ReuseCoroutinue

也可以使用第三方插件写的 MEC (More Efficient Coroutinue :更高效的协程,0 GC),但是我也没用过,只知道有这个东西

    private Coroutine _testingCor1;
    private IEnumerator _testingCor2;
    private UnityCoroutineInst_NoBoxingOperates _testingCor3;

    private IEnumerator TestingCor1()
    {
         yield return 0;
        //_testingCor1 = null;
    }

    private IEnumerator TestingCor2()
    {
        yield return 0;
        //_testingCor2.Reset(); // C# 语法糖生产的没有 Reset 实现,这里会报错
        //_testingCor2 = null;
    }


    private void Check_ReuseCoroutinue()
    {
        Profiler.BeginSample("Check_ReuseCoroutinue");

        Profiler.BeginSample("1");
        if (_testingCor1 == null)
        {
            _testingCor1 = StartCoroutine(TestingCor1());
        }
        Profiler.EndSample();

        Profiler.BeginSample("2");
        if (_testingCor2 == null)
        {
            _testingCor2 = TestingCor2();
        }
        if (_testingCor2 != null)
        {
            if (_testingCor2.MoveNext())
            {
                int v = (int)_testingCor2.Current;
            }
            else
            {
                //_testingCor2.Reset(); // C# 语法糖生产的没有 Reset 实现,这里会报错
            }
        }
        Profiler.EndSample();

        Profiler.BeginSample("3");
        if (_testingCor3 == null)
        {
            _testingCor3 = new UnityCoroutineInst_NoBoxingOperates();
        }
        if (_testingCor3 != null)
        {
            if (_testingCor3.MoveNext())
            {
                int v = _testingCor3.Current;
            }
            else
            {
                _testingCor3.Reset(); // 我们自己实现的 Cortoutine 就可以随心所欲的 Reset,因为自己实现了接口,这样就不用重新 new 一个协程管理对象,也就没有 GC 了
            }
        }
        Profiler.EndSample();

        Profiler.EndSample();

        // Profile 结果
        //  1、2 方式都因为 C# 语法糖内部实际 new 了一个类似 UnityCoroutineInst_NoBoxingOperates 的类来分状态处理,所以每次获取一个 Enumerator 时,都会有 GC
        //  3 方式虽然我们也实现了对应的 IEnumerator,但是我们自己可实现对 Reset 接口的处理,所以不用重新 new,因此只有第一次 new 有 GC
        // 因此,没事不要频繁的 StartCortoutine ,因为有 GC
        // 尽可能使用 Update 函数来处理
    }

在这里插入图片描述

Check_EnumGetValues
    public enum eThreeType
    {
        One,
        Two,
        Three,
    }
    public enum eTenType
    {
        One,
        Two,
        Three,
        Four,
        Five,
        Six,
        Seven,
        Eight,
        Nine,
        Ten,
    }
    private void Check_EnumGetValues()
    {
        Profiler.BeginSample("Check_EnumGetValues");

        Profiler.BeginSample("1");
        {
            var arr = Enum.GetValues(typeof(eThreeType));
        }
        Profiler.EndSample();

        Profiler.BeginSample("2");
        {
            var arr = Enum.GetValues(typeof(eTenType));
        }
        Profiler.EndSample();

        Profiler.EndSample();

        // Profile 结果
        //  1 方式有 3 次 GC
        //  2 方式有 12 次 GC
        //  枚举的成员数量越多,GC越多次,因此,最好将 Enum.GetValues(typeof(T)) 的内容缓存到一个 static 对象中,这样就只会缓存一次,也只会在初始化类时GC
    }

在这里插入图片描述

Check_Lambda
    public delegate void ModVal(ref int v);
    private void ModVal_Method(ref int v)
    {
        v += 1;
    }
    private void Check_Lambda()
    {
        Profiler.BeginSample("Check_Lambda");

        const int LOOP_MAX = 10000;

        {
            Profiler.BeginSample("1");
            ModVal act = (ref int a) => 
            {
                a += 1;
            };
            int v = 0;
            for (int i = 0; i             
关注
打赏
1688896170
查看更多评论
0.0588s