Unity的腳本團隊一直在尋找讓程式更快執行的方法。本系列三篇文章的第一篇將為大家介紹IL2CPP AOT編譯器的小優化方式與如何利用它們。儘管這些小優化並不能讓您的程式執行速度提高2-3倍,但它們會在遊戲中起到重要作用,我們希望這些優化能讓您對程式的執行方式有更深入的理解。
近代的編譯器都很擅於執行一些優化來提高程式的執行效率。作為開發者,我們經常可以透過已知功能的程式,透過一定管道傳達給編譯器來幫助編譯器提高效率。本文將詳細講解一個關於 IL2CPP的小優化,並看看它將如何提高現有程式的執行效率。
去虛擬化(Devirtualization)
眾所周知,虛擬方法的呼叫通常比直接呼叫來的耗效能。我們一直致力於改善libil2cpp運行庫的效能以降低虛擬方法呼叫的消耗(下篇文章將介紹更多內容),但某些排序演算法仍然需要一些執行時的搜查。編譯器無法知道哪些方法會在執行時被呼叫,亦或可不可以被呼叫?
去虛擬化是一個很常見的編譯器優化方案,也就是將虛擬方法呼叫改為直接的方法呼叫。當編譯器編譯時可以提供準確的“實際”方法時,這個方案就會被啟用。不幸的是通常很難做到,因為編譯器通常無法看到整個代碼庫。然而如果可以的話,虛擬方法的呼叫會更快速。
典型例子
作為一個年輕的開發者,我學習虛擬方法是從一個常見的動物案例開始的。可能這個例子你看了很眼熟:
然後在 Unity (版本 5.3.5) 中,我們可以使用這些類別來做個小農場:
這裡每次呼叫Speak的都是虛擬方法呼叫。讓我們看看 IL2CPP去虛擬化這些方法呼叫是如何提高效能的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
public abstract class Animal {
public abstract string Speak();
}
public class Cow : Animal {
public override string Speak() {
return "Moo";
}
}
public class Pig : Animal {
public override string Speak() {
return "Oink";
}
}
|
1
2
3
4
5
6
7
8
9
10
|
public class Farm: MonoBehaviour {
void Start () {
Animal[] animals = new Animal[] {new Cow(), new Pig()};
foreach (var animal in animals)
Debug.LogFormat("Some animal says '{0}'", animal.Speak());
var cow = new Cow();
Debug.LogFormat("The cow says '{0}'", cow.Speak());
}
}
|
產生的C++代碼還不錯
我喜歡IL2CPP的一個功能就是它會產生C++程式碼而非組合語言。當然,這些產生的程式與一般人寫的看起來並不一樣,但這種代碼要比組合語言容易理解得多。下面看看產生的 foreach迴圈:
1
2
3
4
5
6
7
8
9
10
11
|
// Set up a local variable to point to the animal array
AnimalU5BU5D_t2837741914* L_5 = V_2;
int32_t L_6 = V_3;
int32_t L_7 = L_6;
// Get the current animal from the array
V_1 = ((L_5)->GetAt(static_cast<il2cpp_array_size_t>(L_7)));
Animal_t3277885659 * L_9 = V_1;
// Call the Speak method
String_t* L_10 = VirtFuncInvoker0< String_t* >::Invoke(4 /* System.String AssemblyCSharp.Animal::Speak() */, L_9);
|
這裡移除了一些產生的程式來簡化內容。看到那個醜陋的Invoke呼叫了麼?它將會在 vtable中查詢適當的虛擬方法並進行呼叫。vtable的查詢會比直接的函式呼叫慢一些,這可以理解,因為 Animal(動物)可能是 Cow(牛)或者Pig(豬),或者一些其它的派生類型。
下面看看產生的程式中的第二個呼叫Debug.LogFormat,這個看起來更像是直接呼叫:
1
2
3
4
5
6
7
8
|
// Create a new cow
Cow_t1312235562 * L_14 = (Cow_t1312235562 *)il2cpp_codegen_object_new(Cow_t1312235562_il2cpp_TypeInfo_var);
Cow__ctor_m2285919473(L_14, /*hidden argument*/NULL);
V_4 = L_14;
Cow_t1312235562 * L_16 = V_4;
// Call the Speak method
String_t* L_17 = VirtFuncInvoker0< String_t* >::Invoke(4 /* System.String AssemblyCSharp.Cow::Speak() */, L_16);
|
這個例子中仍然使用的是虛擬方法呼叫! 事實上IL2CPP對於優化非常保守,絕大多數情況下會優先確保正確性。由於它並未對整個專案做完全的分析來確保可以進行直接呼叫,因而選擇了更安全(也慢)的虛擬方法呼叫。
假設我們知道農場中沒有其它類型的牛了,因此Cow(牛)這個類別不會產生衍生類別。如果我們清楚告知編譯器這點,就能獲得一個更好的結果。現將類別的定義改為如下:
1
2
3
4
5
|
public sealed class Cow : Animal {
public override string Speak() {
return "Moo";
}
}
|
關鍵字sealed告訴編輯器 Cow(牛)不會有衍生類(sealed也可以直接應用於 Speak方法)。現在 IL2CPP能進行直接呼叫了:
1
2
3
4
5
6
7
8
|
// Create a new cow
Cow_t1312235562 * L_14 = (Cow_t1312235562 *)il2cpp_codegen_object_new(Cow_t1312235562_il2cpp_TypeInfo_var);
Cow__ctor_m2285919473(L_14, /*hidden argument*/NULL);
V_4 = L_14;
Cow_t1312235562 * L_16 = V_4;
// Look ma, no virtual call!
String_t* L_17 = Cow_Speak_m1607867742(L_16, /*hidden argument*/NULL);
|
這裡呼叫 Speak就不會慢了,因為我們已經清楚的告訴編譯器,並且有把握地允許編譯器進行優化。
這種優化不會使你的遊戲執行速度顯著變快。但這對於後來的人閱讀程式和編輯器都會有效許多。如果您想使用IL2CPP編譯,強烈建議仔細閱讀專案產生後的C++代碼,可能會有意想不到的收穫!
下一篇我們將討論為什麼虛擬方法呼叫消耗高,以及怎樣使它變得更快。
您好, 想要請教您有關升級64位元問題。據Google Play要求2019年8月前所有APP都必須升級為64位元,我的APP原先是用unity5.6.1f1,原本以為只要使用unity支援64位元的版本build APK即可升級為64位元,但事實卻沒有那麼簡單,在IL2CPP編譯地方一直有問題,所以想要請教您,從32位元(unity5.6.1f1)升級到64位元,應該使用那個unity版本?程式裡面需要做什麼調整嗎?謝謝。
回覆刪除