Unity匿名函數的堆內存優化
原文鏈接:Unity匿名函數的堆內存優化 - Blog
這是侑虎科技第212篇原創文章,作者仲光澤(QQ:593172),歡迎轉發分享,未經作者授權請勿轉載。如果您有任何獨到的見解或者發現也歡迎聯繫我們,一起探討。(QQ群465082844)
同時,作者也是U Sparkle活動參與者哦,UWA歡迎更多開發朋友加入 U Sparkle開發者計劃,這個舞台有你更精彩!
起因
在一次性能優化中,需要將很多對象緩存起來,因為它們會被經常訪問,每次重新獲取都會產生新的alloc,這個是需要避免的。
我把所有對象丟進了一個Dictionary里,再需要的時候就遍歷一遍。為了方便,我又封裝了一個的foreach,類似下圖。1 public static class Utilsn2 {n3 public static void Forecah<TKey, TValue>(this Dictionary<TKey, TValue> dict, n4 System.Action<TKey, TValue> EnumeratorFunc)n5 {n6 if (dict == null || EnumeratorFunc == null)n7 throw new System.ArgumentNullException();n8 var i = dict.GetEnumerator();n9 while (i.MoveNext())n10 {n11 EnumeratorFunc(i.Current.Key, i.Current.Value);n12 }n13 }n14 }n
然後我上Profiler進行測試,發現還是有104B的alloc。同事提醒我在匿名函數里最好別用這個。修改了實現後,發現alloc終於為0了,但為什麼匿名函數會有alloc(每次調用)呢?我決定探個究竟。
分析
一、測試環境:Win10,Unity5.3.4f1,VS2013
測試數據:
1 public class lambdaTest : MonoBehaviourn2 {n3 Dictionary<int, int> table = new Dictionary<int, int>();n4 public int count;n5 void Start()n6 {n7 table.Add(1, 1);n8 table.Add(2, 2);n9 table.Add(3, 3);n10 table.Add(4, 4);n11 table.Add(5, 5);n12 count = 0;n13 }n14 }n
二、在Update函數里測試各種情況,目前分2種情況:
1. 匿名函數中,未使用外部變數- 匿名函數
- 成員函數
2. 匿名函數中,使用外部變數
- 匿名函數
- 成員函數
1 void Update()n2 {n3 Profiler.BeginSample("AnonymousWithoutParam"); // 未使用外部變數的匿名函數n4 AnonymousWithoutVariable();n5 Profiler.EndSample();n6 Profiler.BeginSample("FunctionWithoutVariable"); // 未使用外部變數的成員函數n7 FunctionWithoutVariable();n8 Profiler.EndSample();n9 Profiler.BeginSample("AnonymousParam"); // 使用外部變數的匿名函數n10 AnonymousVariable();n11 Profiler.EndSample();n12 Profiler.BeginSample("FunctionVariable"); // 使用外部變數的成員函數n13 FunctionVariable();n14 Profiler.EndSample();n15 }n
各類函數代碼
1.未使用外部變數的匿名函數
1 void AnonymousWithoutVariable()n2 {n3 table.Forecah((k, v) =>n4 {n5 int c = 0;n6 c = k + v;n7 });n8 }n
2.未使用外部變數的成員函數
1 void FunctionWithoutVariable()n2 {n3 table.Forecah(AddWithoutVariable);n4 }n5 void AddWithoutVariable(int k, int v)n6 {n7 int c = 0;n8 c = k + v;n9 }n
3.使用外部變數的匿名函數
1 void AnonymousVariable()n2 {n3 table.Forecah((k, v) =>n4 {n5 count = k + v;n6 });n7 }n
4.使用外部變數的成員函數
1 void FunctionVariable()n2 {n3 table.Forecah(AddtVariable);n4 }n5 void AddtVariable(int k, int v)n6 {n7 count = k + v;n8 }n
然後,我們把腳本掛在場景的攝像機上,掛上Profiler開始測試,得到結果如下:
除了第一個函數(AnonymousWithoutVariable)外都有104B的alloc。這是個神奇的現象,但這個時候看代碼已經沒意義了,還是去看IL吧。 用ildasm解開代碼,得到以下內容:
這裡看到,編譯器增加了一堆東西,其中比較關鍵的是:1. 靜態Action聲明
1 .field private static class [System.Core]System.Action`2<int32,int32> CS$<>9__CachedAnonymousMethodDelegate1n
2. 靜態成員函數
.method private hidebysig static void <AnonymousWithoutVariable>b__0(int32 k, int32 v) cil managedn
3. 非靜態成員函數
.method private hidebysig instance void <AnonymousVariable>b__2(int32 k, int32 v) cil managedn
我們先從沒有產生alloc的函數開始分析。 這個函數沒有使用外部變數,那麼編譯器就會把這個函數變成一個靜態函數。 在調用的時候才會new出來,但一旦new出來之後就不會再new了,這就是它為什麼沒有alloc的原因。
調用函數的IL代碼(注意IL_000C行的注釋)
1 .method private hidebysig instance void AnonymousWithoutVariable() cil managedn2 {n3 // 代碼大小 45 (0x2d)n4 .maxstack 8n5 IL_0000: nopn6 IL_0001: ldarg.0n7 IL_0002: ldfld class [mscorlib]System.Collections.Generic.Dictionary2<int32,int32> n8 lambaTest::tablen9 IL_0007: ldsfld class [System.Core]System.Action2<int32,int32> n10 lambaTest::CS$<>9__CachedAnonymousMethodDelegate1n11 IL_000c: brtrue.s IL_0021 // 判斷這個函數是否new過,沒new就往下走,否則就跳到IL_0021行。n12 IL_000e: ldnulln13 IL_000f: ldftn void lambaTest::<AnonymousWithoutVariable>b__0(int32, int32)n14 IL_0015: newobj instance void class n15 [System.Core]System.Action2<int32,int32>::.ctor(object, native int)n16 IL_001a: stsfld class [System.Core]System.Action2<int32,int32> n17 lambaTest::CS$<>9__CachedAnonymousMethodDelegate1n18 IL_001f: br.s IL_0021n19 IL_0021: ldsfld class [System.Core]System.Action2<int32,int32> n20 lambaTest::CS$<>9__CachedAnonymousMethodDelegate1n21 IL_0026: call void Utils::Forecah<int32,int32>(class n22 [mscorlib]System.Collections.Generic.Dictionary2<!!0,!!1>,n23 class n24 [System.Core]System.Action2<!!0,!!1>)n25 IL_002b: nopn26 IL_002c: retn27 } // end of method lambaTest::AnonymousWithoutVariablen
其他會產生的alloc的函數,都是成員函數(包括匿名的,也變成成員的了),所以每次都會new一個action對象。
一個例子,使用外部變數的IL代碼。其中IL_000e行就會new一個對象。
1 .method private hidebysig instance void AnonymousVariable() cil managedn2 {n3 // 代碼大小 26 (0x1a)n4 .maxstack 8n5 IL_0000: nopn6 IL_0001: ldarg.0n7 IL_0002: ldfld class [mscorlib]System.Collections.Generic.Dictionary`2<int32,int32>n8 lambaTest::tablen9 IL_0007: ldarg.0n10 IL_0008: ldftn instance void lambaTest::<AnonymousVariable>b__2(int32, int32)n11 IL_000e: newobj instance void classn12 [System.Core]System.Action`2<int32,int32>::.ctor(object, native int)n13 IL_0013: call void Utils::Forecah<int32,int32>(classn14 [mscorlib]System.Collections.Generic.Dictionary`2<!!0,!!1>,n15 class n16 [System.Core]System.Action`2<!!0,!!1>)n17 IL_0018: nopn18 IL_0019: retn19 } // end of method lambaTest::AnonymousVariablen
第二個例子。
1 .method private hidebysig instance void FunctionWithoutVariable() cil managedn2 {n3 // 代碼大小 26 (0x1a)n4 .maxstack 8n5 IL_0000: nopn6 IL_0001: ldarg.0n7 IL_0002: ldfld class [mscorlib]System.Collections.Generic.Dictionary2<int32,int32>n8 lambaTest::tablen9 IL_0007: ldarg.0n10 IL_0008: ldftn instance void lambaTest::AddWithoutVariable(int32, int32)n11 IL_000e: newobj instance void class n12 [System.Core]System.Action2<int32,int32>::.ctor(object, native int)n13 IL_0013: call void Utils::Forecah<int32,int32>(class n14 [mscorlib]System.Collections.Generic.Dictionary2<!!0,!!1>,n15 class n16 [System.Core]System.Action2<!!0,!!1>)n17 IL_0018: nopn18 IL_0019: retn19 } // end of method lambaTest::FunctionWithoutVariablen
結論
當不使用外部變數的匿名函數時,編譯器會把這個函數變成靜態函數,在首次調用時初始化,之後就再也不會new新的對象。 當使用外部變數時,每次調用都會生成一個臨時action變數,這個就是alloc的原因。
解決辦法以及相關建議
這裡會麻煩一點,需要聲明一下:
1 public Action<int, int> pCall;n2 void Start()n3 {n4 pCall = CallVariable;n5 ... // 其他初始化代碼n6 }n7 void FixedCall()n8 {n9 table.Forecah(pCall);n10 }n11 void CallVariable(int k, int v)n12 {n13 count = k + v;n14 }n
測試結果
FixedCall是實際的函數名,aaaaa是profiler採樣的函數名相關建議
在高頻調用時,研發團隊可以考慮通過聲明一個變數來解決或者不要使用,如果調用頻率很低,那就不用去關心了。相關測試代碼
jlu3389/UnityLambdaAllocTest文末,再次感謝仲光澤的分享,如果您有任何獨到的見解或者發現也歡迎聯繫我們,一起探討。(QQ群465082844)。
也歡迎大家來積极參与U Sparkle開發者計劃,簡稱"US",代表你和我,代表UWA和開發者在一起!推薦閱讀:
※一個採用GC的原生程序語言有沒有可能性能上超越非GC的原生程序語言?
※寫工業級別代碼是怎樣一種體驗?
※一段簡單文件IO的C程序,為什麼我的優化反而更慢?
※如何優化QTableView的性能?
※你見過哪些令你瞠目結舌的C/C++代碼技巧?