(八)Unity5.0新特性------IL2CPP Internals: 產生的程式碼之旅,unity5.0

來源:互聯網
上載者:User

(八)Unity5.0新特性------IL2CPP Internals: 產生的程式碼之旅,unity5.0

孫廣東  2015.5.25

轉載請註明出處吧

這是 IL2CPP Internals系列中的第二個部落格文章。在這篇文章,我們將探討由 il2cpp.exe 產生的 c + + 代碼。一路走來,我們將看到託管的類型怎麼樣表示在機器碼中,看看運行時檢查用來支援.NET 虛擬機器,請參閱如何迴圈產生的更多 !
     我們會遇到一些非常特定於版本的代碼,更高版本的Unity一定會改變。儘管如此,但概念將保持不變。
樣本項目:
我會為此樣本使用Unity5.0.1p1 最新版本。在本系列的第一篇,我會從空項目開始,並添加一個指令檔。這一次,它具有下列內容:
using UnityEngine;
public class HelloWorld : MonoBehaviour {
  private class Important {
    public static int ClassIdentifier = 42;
    public int InstanceIdentifier;
  }
  void Start () {
    Debug.Log("Hello, IL2CPP!");
    Debug.LogFormat("Static field: {0}", Important.ClassIdentifier);
    var importantData = new [] {
      new Important { InstanceIdentifier = 0 },
      new Important { InstanceIdentifier = 1 } };
    Debug.LogFormat("First value: {0}", importantData[0].InstanceIdentifier);
    Debug.LogFormat("Second value: {0}", importantData[1].InstanceIdentifier);
    try {
      throw new InvalidOperationException("Don't panic");
    }
    catch (InvalidOperationException e) {
      Debug.Log(e.Message);
    }
    for (var i = 0; i < 3; ++i) {
      Debug.LogFormat("Loop iteration: {0}", i);
    }
  }
}

我就會在 Windows 上運行 Unity 編輯器為 WebGL,建立這一項目。我選擇Development Player選項在Build Settings中,這樣我們可以得到相對不錯的名字產生的 c + + 代碼中。我也已經設定Enable Exceptions選項在 WebGL Player Settings中值為Full。

產生的程式碼的概述:
     WebGL build完成後,產生的 c + + 代碼是在我的項目目錄中的 Temp\StagingArea\Data\il2cppOutput 目錄中。一旦關閉了編輯器,將刪除此目錄。只要編輯器是開啟的此目錄將保持不變,所以我們可以檢查它。

Il2cpp.exe 公用程式產生的檔案的數目,甚至小項目。4625個 標頭檔和 89 c + + 原始碼檔案, 若要獲得此代碼的所有控制代碼,我喜歡使用一個文字編輯器 Exuberant CTags。CTags 通常會迅速產生為這段代碼的標籤檔案,這使得它易於導航。

最初,你可以看到很多產生的 c + + 檔案,而不是從簡單的指令碼代碼,但相反的轉換版本代碼在標準庫,如 mscorlib.dll 中的代碼。在本系列中的第一篇文章中提到,IL2CPP 指令碼後端使用相同的標準庫代碼作為Mono的指令碼後端。注意: 我們轉換 mscorlib.dll 和其他標準庫程式集,每個時間 il2cpp.exe 運行中的代碼。這可能似乎不必要的因為該代碼不會改變。

然而,IL2CPP 指令碼後端始終使用位元組代碼剝離減小該可執行檔的大小。因此,即使在指令碼代碼中微小的變化可以引發許多不同的部分要使用標準庫代碼,根據具體情況。因此,我們需要每次轉換將 mscorlib.dll 程式集。我們正在研究如何更好地做增量產生,但我們還沒有任何好的解決方案。


如何將託管的代碼映射到產生的 c + + 代碼:

對於在Managed 程式碼中的每個類型,il2cpp.exe 將產生一個 c + +的標頭檔 中定義類型 和類型的方法聲明在另一個標頭檔。例如,讓我們看看轉換後的 UnityEngine.Vector3 類型的內容。標頭檔的類型是命名為 UnityEngine_UnityEngine_Vector3.h。建立了一種基於程式集名稱,UnityEngine.dll 緊跟的命名空間和類型的名稱的名稱。代碼如下所示:
// UnityEngine.Vector3
struct Vector3_t78
{
  // System.Single UnityEngine.Vector3::x
  float ___x_1;
  // System.Single UnityEngine.Vector3::y
  float ___y_2;
  // System.Single UnityEngine.Vector3::z
  float ___z_3;
};

Il2cpp.exe 公用程式已轉換三個執行個體欄位,並且做一點點的名稱重整以避免和保留字衝突。通過使用前置底線,我們在 c + + 中使用一些保留的名稱,但到目前為止,我們沒看到任何衝突與 c + + 標準庫代碼。
UnityEngine_UnityEngine_Vector3MethodDeclarations.h 檔案中包含的所有Vector3方法聲明。例如,Vector3 重寫 Object.ToString 方法:

// System.String UnityEngine.Vector3::ToString()
extern "C" String_t* Vector3_ToString_m2315 (Vector3_t78 * __this, MethodInfo* method) IL2CPP_METHOD_ATTR
 
注意注釋,指示此本機聲明表示的託管的方法。我經常發現搜尋檔案中輸出託管方法在此格式中有用,尤其是對於以常見的名稱如 tostring。
通知都由 il2cpp.exe 轉換的方法是有趣的事情:

    · 在c++中這些不是成員函數。所有方法都是free functions,其中的第一個參數是"this"指標。關於Managed 程式碼中的靜態函數,IL2CPP 總是讓這第一個參數this設為 NULL 值。通過始終聲明具有"this"指標作為第一個參數的方法,我們簡化了il2cpp.exe產生代碼方法和我們使其他方法 (如委託) 的調用方法產生的程式碼更簡單。

 · 每個方法有一個額外的參數的類型是MethodInfo *,其中包括用於類的虛擬方法調用的有關方法的中繼資料。Mono的指令碼後端使用特定於平台的trampolines傳遞此中繼資料。關於IL2CPP,我們已經決定避免使用trampolines,有助於可移植性。

 · 所有方法都聲明 extern "C",以便 il2cpp.exe 有時可以對 c++ 編譯器撒謊和對待所有方法,因為如果他們有相同的類型。

 · 以"_t"尾碼命名的類型。以"_m"尾碼命名的方法。命名衝突解決的每個名稱後附加一個唯一的數字。如果任何使用者指令碼代碼中發生更改這些數字將會改變,所以你不能在build時指望他們。

 前兩個點暗示每個方法有至少兩個參數,是"this"指標和 MethodInfo 指標。這些額外的參數會導致不必要的開銷嗎?儘管他們會增加開銷,到目前為止我們從來沒有見過那些額外的參數會導致效能問題。儘管它看起來他們可能會導致,分析表明效能的差異是不可以衡量太小了。

 我們可以使用 Ctags 工具跳轉到這個 ToString 方法的定義。它是在 Bulk_UnityEngine_0.cpp 檔案中。代碼中該方法的定義看起來不太像 C# 代碼中的 Vector3::ToString() 方法。然而,如果你使用像 ILSpy 這樣的工具來反射 Vector3::ToString() 方法的代碼,您將看到產生的 c + + 代碼看起來非常類似於 IL 代碼。

 為什麼 il2cpp.exe 不會為每個類型的一樣的方法聲明分別產生一個單獨的 c + + 檔案,這個 Bulk_UnityEngine_0.cpp 檔案是相當大,其實20,481 行 !我們發現我們正在使用的 c + + 編譯器有大量的原始碼檔案的麻煩。編譯四千個.cpp 檔案時間遠遠多於 80 個.cpp相同的原始碼 檔案編譯。所以 il2cpp.exe 類型分組的批方法定義,並每個組產生一個 c + + 檔案,。

現在跳回方法聲明的標頭檔,並注意到該檔案的頂部附近的這行:
#include "codegen/il2cpp-codegen.h"


il2cpp-codegen.h檔案包含產生的程式碼用來訪問 libil2cpp 運行時服務的介面。我們將討論一些運行時使用的方法產生的程式碼。

Method prologues
 讓我們看看 Vector3::ToString() 方法的定義。具體說來,它具有共同的prologue部分,由 il2cpp.exe emitted的所有方法。


StackTraceSentry _stackTraceSentry(&Vector3_ToString_m2315_MethodInfo);
static bool Vector3_ToString_m2315_init;
if (!Vector3_ToString_m2315_init)
{
  ObjectU5BU5D_t4_il2cpp_TypeInfo_var = il2cpp_codegen_class_from_type(&ObjectU5BU5D_t4_0_0_0);
  Vector3_ToString_m2315_init = true;
}
 

 這樣的prologue的第一行建立一個局部變數的類型 StackTraceSentry。此變數用於跟蹤託管的呼叫堆疊,因此,IL2CPP 可以報告它在調用像 Environment.StackTrace。此條目的代碼產生是實際上是可選的並在這種情況下啟用的--啟用棧跟蹤選項傳遞給 il2cpp.exe (因為我在WebGL Player Settings中設定Enable Exceptions選項為Full)。對於小函數,我們發現此變數的開銷對效能有負面的影響。所以對於 iOS 和其他平台,在那裡我們可以使用特定於平台的堆疊追蹤資訊,我們永遠不會發出這條線到產生的程式碼。WebGL,我們沒有特定於平台的堆疊追蹤支援,因此有必要允許託管的代碼異常才能正常工作。

 prologue的第二部分沒有延遲初始化的數組或在方法體中使用的泛型型別的類型中繼資料。所以名稱 ObjectU5BU5D_t4 是類型名為 System.Object [] 。prologue的這一部分只執行一次,並經常做什麼如果類型已初始化在其他地方,所以我們還沒有看到任何負面效能影響從產生的程式碼。

 可是此代碼安全執行緒嗎?如果兩個線程同時調用 Vector3::ToString()?其實,此代碼並不成問題,因為所有的 libil2cpp 運行時用於類型中的代碼初始化是安全的從多個線程中調用。它是可能 (甚至可能) 會不止一次,調用 il2cpp_codegen_class_from_type 函數,但它的實際工作才會有一次,發生在一個線程上。方法執行不會繼續,直到初始化已完成。所以這方法開場白是安全執行緒的。

Runtime checks運行時檢查
 該方法的下一部分建立一個對象數組、 Vector3的 x 欄位的值儲存在本地,然後盒當地和將其添加到索引從零開始的數組。下面是產生的 c + + 代碼 (用一些注釋功能):
 
 // Create a new single-dimension, zero-based object array
 ObjectU5BU5D_t4* L_0 = ((ObjectU5BU5D_t4*)SZArrayNew(ObjectU5BU5D_t4_il2cpp_TypeInfo_var, 3));
 // Store the Vector3::x field in a local
 float L_1 = (__this->___x_1);
 float L_2 = L_1;
 // Box the float instance, since it is a value type.
 Object_t * L_3 = Box(InitializedTypeInfo(&Single_t264_il2cpp_TypeInfo), &L_2);
 // Here are three important runtime checks
 NullCheck(L_0);
 IL2CPP_ARRAY_BOUNDS_CHECK(L_0, 0);
 ArrayElementTypeCheck (L_0, L_3);
 // Store the boxed value in the array at index 0
 *((Object_t **)(Object_t **)SZArrayLdElema(L_0, 0)) = (Object_t *)L_3;
 

  三個運行時檢查不存在 IL 代碼中,但反而被由 il2cpp.exe注入。
• 該 NullCheck 代碼將引發NullReferenceException,如果數組的值為 null。
• 該 IL2CPP_ARRAY_BOUNDS_CHECK 代碼將引發 IndexOutOfRangeException,如果數組索引不正確。
• 該 ArrayElementTypeCheck 代碼會引發的 ArrayTypeMismatchException,如果被添加到該數組中元素的類型不正確。

 這些三個運行時檢查是由.NET 虛擬機器提供的所有保證。而不是注入代碼,Mono指令碼後端使用平台特定的訊號轉導機制來處理這些相同的運行時檢查。對於 IL2CPP,我們想要更多的平台得到不可知論的和支援的平台,像 WebGL,那裡有沒有特定於平台的訊號轉導機制,所以 il2cpp.exe 注入這些檢查。
 
 做這些運行時檢查會導致效能問題嗎?在大多數情況下,在效能上,我們沒看到任何不利的影響,他們提供的好和.NET 虛擬機器所需的安全。不過,在幾個特定的情況下我們看到這些檢查,導致效能下降,尤其是在緊湊迴圈中。我們正在做現在允許託管的代碼進行注釋以移除這些運行時檢查,當 il2cpp.exe 產生 c + + 代碼。敬請關注這一方面。
 
Static Fields靜態欄位
 現在,我們已經看到如何執行個體欄位(Vector3 類型),讓我們看到了靜態欄位轉換和訪問。找到的 HelloWorld_Start_m3 方法定義,是在我產生的 Bulk_Assembly CSharp_0.cpp 檔案中定義。從那裡,跳轉到 Important_t1 類型 (在 theAssemblyU2DCSharp_HelloWorld_Important.h 檔案中):

struct Important_t1  : public Object_t
{
  // System.Int32 HelloWorld/Important::InstanceIdentifier
  int32_t ___InstanceIdentifier_1;
};
struct Important_t1_StaticFields
{
  // System.Int32 HelloWorld/Important::ClassIdentifier
  int32_t ___ClassIdentifier_0;
};
 

Notice that il2
Notice that il2cpp.exe has generated a separate C++ struct to hold the static field for this type, since the static field is shared between all instances of this type. So at runtime, there will be one instance of the Important_t1_StaticFields type created, and all of the instances of the Important_t1 type will share that instance of the static fields type. In generated code, the static field is accessed like this:
請注意,il2cpp.exe 已對此類型的靜態欄位產生一個單獨的 c + + 結構體,因為該靜態欄位這種類型的所有執行個體之間要共用。所以在運行時,會有建立,Important_t1_StaticFields 類型的一個執行個體,所有 Important_t1 類型的執行個體將共用該執行個體的靜態欄位的類型。產生的程式碼中訪問靜態欄位時像這樣:

int32_t L_1 = (((Important_t1_StaticFields*)InitializedTypeInfo(&Important_t1_il2cpp_TypeInfo)->static_fields)->___ClassIdentifier_0);

Important_t1 的類型中繼資料握著一個指標,指向的 Important_t1_StaticFields 類型的一個執行個體,該執行個體用於擷取靜態欄位的值。

Exceptions例外情況

託管異常被il2cpp.exe轉換為c + +異常。我們選擇了這條道路,以再次避免特定於平台的解決辦法。當 il2cpp.exe 需要emit代碼引發的託管的異常時,它將調用 il2cpp_codegen_raise_exception 函數。
 在我們的 HelloWorld_Start_m3 方法來引發和捕捉託管的異常的代碼如下所示:
try
{ // begin try (depth: 1)
  InvalidOperationException_t7 * L_17 = (InvalidOperationException_t7 *)il2cpp_codegen_object_new (InitializedTypeInfo(&InvalidOperationException_t7_il2cpp_TypeInfo));
  InvalidOperationException__ctor_m8(L_17, (String_t*) &_stringLiteral5, /*hidden argument*/&InvalidOperationException__ctor_m8_MethodInfo);
  il2cpp_codegen_raise_exception(L_17);
  // IL_0092: leave IL_00a8
  goto IL_00a8;
} // end try (depth: 1)
catch(Il2CppExceptionWrapper& e)
{
  __exception_local = (Exception_t8 *)e.ex;
  if(il2cpp_codegen_class_is_assignable_from (&InvalidOperationException_t7_il2cpp_TypeInfo, e.ex->object.klass))
  goto IL_0097;
  throw e;
}
IL_0097:
{ // begin catch(System.InvalidOperationException)
  V_1 = ((InvalidOperationException_t7 *)__exception_local);
  NullCheck(V_1);
  String_t* L_18 = (String_t*)VirtFuncInvoker0< String_t* >::Invoke(&Exception_get_Message_m9_MethodInfo, V_1);
  Debug_Log_m6(NULL /*static, unused*/, L_18, /*hidden argument*/&Debug_Log_m6_MethodInfo);
// IL_00a3: leave IL_00a8
  goto IL_00a8;
} // end catch (depth: 1)
 

所有託管的異常將封裝在 c + + 的Il2CppExceptionWrapper 類型內。當產生的程式碼捕獲該類型的異常時,它解包 (其類型 Exception_t8) 的託管異常的 c + + 表示。我們期待在這種情況下,只為能反轉,所以如果我們找不到該類型的異常的 c + + 異常的副本又扔了回來。如果我們找到正確的類型,該代碼跳轉到的 catch 處理常式,執行並寫出的異常訊息。

Goto!?!
 這段代碼提出了一個有趣的點。這些標籤和 goto 語句在那裡做什嗎?這些構造是不必要的結構化編程 !然而,IL 沒有結構化編程概念,如迴圈及 if/then 語句。因為它是較低層級,il2cpp.exe 遵循低層級概念產生的程式碼中。
例如,讓我們看看 for 迴圈在 HelloWorld_Start_m3 中的方法:
 
IL_00a8:
{
  V_2 = 0;
  goto IL_00cc;
}
IL_00af:
{
  ObjectU5BU5D_t4* L_19 = ((ObjectU5BU5D_t4*)SZArrayNew(ObjectU5BU5D_t4_il2cpp_TypeInfo_var, 1));
  int32_t L_20 = V_2;
  Object_t * L_21 =
Box(InitializedTypeInfo(&Int32_t5_il2cpp_TypeInfo), &L_20);
  NullCheck(L_19);
  IL2CPP_ARRAY_BOUNDS_CHECK(L_19, 0);
  ArrayElementTypeCheck (L_19, L_21);
*((Object_t **)(Object_t **)SZArrayLdElema(L_19, 0)) = (Object_t *)L_21;
  Debug_LogFormat_m7(NULL /*static, unused*/, (String_t*) &_stringLiteral6, L_19, /*hidden argument*/&Debug_LogFormat_m7_MethodInfo);
  V_2 = ((int32_t)(V_2+1));
}
IL_00cc:
{
  if ((((int32_t)V_2) < ((int32_t)3)))
  {
    goto IL_00af;
  }
}
 
 
這裡的 V_2 變數是迴圈索引。是開始的一個值為 0,然後遞增下面這一行中的迴圈:
 V_2 = ((int32_t)(V_2+1));

然後在這裡檢查迴圈的結束條件:
 if ((((int32_t)V_2) < ((int32_t)3)))

只要 V_2 是小於 3,goto 語句跳轉到 IL_00af 標籤,這是迴圈體的頂部。你可能能夠猜出那 il2cpp.exe 器當前正在產生 c + + 代碼直接從 IL,而無需使用中間的抽象文法樹表示形式。如果您猜到這,你是正確的。你可能已經還注意到在運行時檢查上述一些產生的程式碼看起來像這樣:
 float L_1 = (__this->___x_1);
 float L_2 = L_1;
 
顯然,採用 L_2 變數在這裡不是必要的。大多數 c + + 編譯器可以最佳化掉這額外的任務,但是我們想要避免emitting它在所有。我們目前正在研究使用 AST 來更好地理解 IL 代碼和產生更好的 c + + 代碼涉及本地變數的情況下,for 迴圈,其中的可能性。

Conclusion結論
 我們只是抓到一個非常簡單的項目的 IL2CPP 指令碼後端所產生的 c + + 代碼的表面。如果你沒這麼做過,我鼓勵你來到您的項目中產生的程式碼。當你在探索,請牢記我們正在不斷努力提高構建和運行時效能的 IL2CPP 指令碼後端所產生的 c + + 代碼將看上去不同的,未來版本中。

 通過將 IL 代碼轉換為 c + + 中,我們已經能夠獲得很好的平衡,攜帶型和高效能代碼之間。我們可以有很多不錯的開發人員友好功能的Managed 程式碼中,同時仍獲得 c + + 編譯器提供各種平台的品質機器代碼的好處。

 在將來職位,我們會探索更多產生的程式碼,包括方法調用、 分享的方法實現和調用到本機庫的封裝。但下一次我們將調試一些為使用 Xcode iOS 64 位元組建產生的程式碼。

文章的源地址:
http://blogs.unity3d.com/2015/05/13/il2cpp-internals-a-tour-of-generated-code/


聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.