Unity匿名函数的堆内存优化
- 作者:admin
- /
- 时间:2017年03月14日
- /
- 浏览:8983 次
- /
- 分类:厚积薄发
Unity的坑很多,Mono的坑更多,这里记一次解决lambda函数alloc的问题。
起因
在一次性能优化中,需要将很多对象缓存起来,因为它们会被经常访问,每次重新获取都会产生新的alloc,这个是需要避免的。
我把所有对象丢进了一个Dictionary里,再需要的时候就遍历一遍。为了方便,我又封装了一个的foreach,类似下图。
1 public static class Utils
2 {
3 public static void Forecah<TKey, TValue>(this Dictionary<TKey, TValue> dict,
4 System.Action<TKey, TValue> EnumeratorFunc)
5 {
6 if (dict == null || EnumeratorFunc == null)
7 throw new System.ArgumentNullException();
8 var i = dict.GetEnumerator();
9 while (i.MoveNext())
10 {
11 EnumeratorFunc(i.Current.Key, i.Current.Value);
12 }
13 }
14 }
然后我上Profiler进行测试,发现还是有104B的alloc。同事提醒我在匿名函数里最好别用这个。修改了实现后,发现alloc终于为0了,但为什么匿名函数会有alloc(每次调用)呢?我决定探个究竟。
分析
一、测试环境:Win10,Unity5.3.4f1,VS2013
测试数据:
1 public class lambdaTest : MonoBehaviour
2 {
3 Dictionary<int, int> table = new Dictionary<int, int>();
4 public int count;
5 void Start()
6 {
7 table.Add(1, 1);
8 table.Add(2, 2);
9 table.Add(3, 3);
10 table.Add(4, 4);
11 table.Add(5, 5);
12 count = 0;
13 }
14 }
二、在Update函数里测试各种情况,目前分2种情况:
1.匿名函数中,未使用外部变量
- 匿名函数
- 成员函数
2.匿名函数中,使用外部变量
- 匿名函数
- 成员函数
1 void Update()
2 {
3 Profiler.BeginSample("AnonymousWithoutParam"); // 未使用外部变量的匿名函数
4 AnonymousWithoutVariable();
5 Profiler.EndSample();
6 Profiler.BeginSample("FunctionWithoutVariable"); // 未使用外部变量的成员函数
7 FunctionWithoutVariable();
8 Profiler.EndSample();
9 Profiler.BeginSample("AnonymousParam"); // 使用外部变量的匿名函数
10 AnonymousVariable();
11 Profiler.EndSample();
12 Profiler.BeginSample("FunctionVariable"); // 使用外部变量的成员函数
13 FunctionVariable();
14 Profiler.EndSample();
15 }
各类函数代码
1.未使用外部变量的匿名函数
1 void AnonymousWithoutVariable()
2 {
3 table.Forecah((k, v) =>
4 {
5 int c = 0;
6 c = k + v;
7 });
8 }
2.未使用外部变量的成员函数
1 void FunctionWithoutVariable()
2 {
3 table.Forecah(AddWithoutVariable);
4 }
5 void AddWithoutVariable(int k, int v)
6 {
7 int c = 0;
8 c = k + v;
9 }
3.使用外部变量的匿名函数
1 void AnonymousVariable()
2 {
3 table.Forecah((k, v) =>
4 {
5 count = k + v;
6 });
7 }
4.使用外部变量的成员函数
1 void FunctionVariable()
2 {
3 table.Forecah(AddtVariable);
4 }
5 void AddtVariable(int k, int v)
6 {
7 count = k + v;
8 }
然后,我们把脚本挂在场景的摄像机上,挂上Profiler开始测试,得到结果如下:
除了第一个函数(AnonymousWithoutVariable)外都有104B的alloc。这是个神奇的现象,但这个时候看代码已经没意义了,还是去看IL吧。 用ildasm解开代码,得到以下内容:
这里看到,编译器增加了一堆东西,其中比较关键的是:
1. 静态Action声明
1 .field private static class [System.Core]System.Action`2<int32,int32> 'CS$<>9__CachedAnonymousMethodDelegate1'
2. 静态成员函数
.method private hidebysig static void '<AnonymousWithoutVariable>b__0'(int32 k, int32 v) cil managed
3. 非静态成员函数
.method private hidebysig instance void '<AnonymousVariable>b__2'(int32 k, int32 v) cil managed
我们先从没有产生alloc的函数开始分析。 这个函数没有使用外部变量,那么编译器就会把这个函数变成一个静态函数。 在调用的时候才会new出来,但一旦new出来之后就不会再new了,这就是它为什么没有alloc的原因。
调用函数的IL代码(注意IL_000C行的注释)
1 .method private hidebysig instance void AnonymousWithoutVariable() cil managed
2 {
3 // 代码大小 45 (0x2d)
4 .maxstack 8
5 IL_0000: nop
6 IL_0001: ldarg.0
7 IL_0002: ldfld class [mscorlib]System.Collections.Generic.Dictionary2<int32,int32>
8 lambaTest::table
9 IL_0007: ldsfld class [System.Core]System.Action2<int32,int32>
10 lambaTest::'CS$<>9__CachedAnonymousMethodDelegate1'
11 IL_000c: brtrue.s IL_0021 // 判断这个函数是否new过,没new就往下走,否则就跳到IL_0021行。
12 IL_000e: ldnull
13 IL_000f: ldftn void lambaTest::'<AnonymousWithoutVariable>b__0'(int32, int32)
14 IL_0015: newobj instance void class
15 [System.Core]System.Action2<int32,int32>::.ctor(object, native int)
16 IL_001a: stsfld class [System.Core]System.Action2<int32,int32>
17 lambaTest::'CS$<>9__CachedAnonymousMethodDelegate1'
18 IL_001f: br.s IL_0021
19 IL_0021: ldsfld class [System.Core]System.Action2<int32,int32>
20 lambaTest::'CS$<>9__CachedAnonymousMethodDelegate1'
21 IL_0026: call void Utils::Forecah<int32,int32>(class
22 [mscorlib]System.Collections.Generic.Dictionary2<!!0,!!1>,
23 class
24 [System.Core]System.Action2<!!0,!!1>)
25 IL_002b: nop
26 IL_002c: ret
27 } // end of method lambaTest::AnonymousWithoutVariable
其他会产生的alloc的函数,都是成员函数(包括匿名的,也变成成员的了),所以每次都会new一个action对象。
一个例子,使用外部变量的IL代码。其中IL_000e行就会new一个对象。
1 .method private hidebysig instance void AnonymousVariable() cil managed
2 {
3 // 代码大小 26 (0x1a)
4 .maxstack 8
5 IL_0000: nop
6 IL_0001: ldarg.0
7 IL_0002: ldfld class [mscorlib]System.Collections.Generic.Dictionary`2<int32,int32>
8 lambaTest::table
9 IL_0007: ldarg.0
10 IL_0008: ldftn instance void lambaTest::'<AnonymousVariable>b__2'(int32, int32)
11 IL_000e: newobj instance void class
12 [System.Core]System.Action`2<int32,int32>::.ctor(object, native int)
13 IL_0013: call void Utils::Forecah<int32,int32>(class
14 [mscorlib]System.Collections.Generic.Dictionary`2<!!0,!!1>,
15 class
16 [System.Core]System.Action`2<!!0,!!1>)
17 IL_0018: nop
18 IL_0019: ret
19 } // end of method lambaTest::AnonymousVariable
第二个例子。
1 .method private hidebysig instance void FunctionWithoutVariable() cil managed
2 {
3 // 代码大小 26 (0x1a)
4 .maxstack 8
5 IL_0000: nop
6 IL_0001: ldarg.0
7 IL_0002: ldfld class [mscorlib]System.Collections.Generic.Dictionary2<int32,int32>
8 lambaTest::table
9 IL_0007: ldarg.0
10 IL_0008: ldftn instance void lambaTest::AddWithoutVariable(int32, int32)
11 IL_000e: newobj instance void class
12 [System.Core]System.Action2<int32,int32>::.ctor(object, native int)
13 IL_0013: call void Utils::Forecah<int32,int32>(class
14 [mscorlib]System.Collections.Generic.Dictionary2<!!0,!!1>,
15 class
16 [System.Core]System.Action2<!!0,!!1>)
17 IL_0018: nop
18 IL_0019: ret
19 } // end of method lambaTest::FunctionWithoutVariable
结论
当不使用外部变量的匿名函数时,编译器会把这个函数变成静态函数,在首次调用时初始化,之后就再也不会new新的对象。 当使用外部变量时,每次调用都会生成一个临时action变量,这个就是alloc的原因。
解决办法以及相关建议
这里会麻烦一点,需要声明一下:
1 public Action<int, int> pCall;
2 void Start()
3 {
4 pCall = CallVariable;
5 ... // 其他初始化代码
6 }
7 void FixedCall()
8 {
9 table.Forecah(pCall);
10 }
11 void CallVariable(int k, int v)
12 {
13 count = k + v;
14 }
测试结果
FixedCall是实际的函数名,aaaaa是profiler采样的函数名
相关建议
在高频调用时,研发团队可以考虑通过声明一个变量来解决或者不要使用,如果调用频率很低,那就不用去关心了。
相关测试代码
https://github.com/jlu3389/UnityLambdaAllocTest
这是侑虎科技第212篇原创文章,作者仲光泽(QQ:593172),欢迎转发分享,未经作者授权请勿转载。如果您有任何独到的见解或者发现也欢迎联系我们,一起探讨。(QQ群:793972859)
同时,作者也是U Sparkle活动参与者哦,UWA欢迎更多开发朋友加入 U Sparkle开发者计划,这个舞台有你更精彩!
其实不用看IL代码,直接用Reflector把.NET的平台选成None看没有语法糖的代码就看出来会new一个匿名对象。
强势留言!