跟Unity學代碼優化

来源:http://www.cnblogs.com/fingerpass/archive/2016/09/02/how-il2cpp-optimizes-code.html
-Advertisement-
Play Games

今天我們來聊聊如何跟Unity學代碼優化,準確地說,是通過學習Unity的IL2CPP技術的優化策略,應用到我們的日常邏輯開發中。 ...


今天我們來聊聊如何跟Unity學代碼優化,準確地說,是通過學習Unity的IL2CPP技術的優化策略,應用到我們的日常邏輯開發中。

 

做過Unity開發的同學想必對IL2CPP都很清楚,簡單地說,IL2CPP就是Unity用來替代mono的一種script backend。至於說Unity為什麼用IL2CPP替代mono,就是另外的話題了,本文就不細港了。

 

IL2CPP由兩部分組成:

  • 一個AOT(ahead of time)compiler。完全用C#寫的。

  • 一個VM runtime library。主體C++,外加部分平臺特定的彙編代碼。

 

IL2CPP AOT compiler的工作原理就如字面意思,讀取並Parse (雖然並不知道用Mono.Cecil算不算Parse)IL Assembly ,分析並優化,然後生成cpp代碼。IL2CPP的實現也很簡單,生成的C++代碼基本跟IL一一對應,有興趣的同學可以自己試一下寫點C#,然後看看生成的C++代碼。

 

IL2CPP正式release已經有一年多了,一開始人人質疑,現在大家已經基本接受。這種轉變肯定不是一日促成的,主要還是靠Unity對IL2CPP的重視和持續跟進的優化。

這兩個月,Unity官博發了一個IL2CPP優化三部曲,接下來我們就看看如何從其中學習代碼優化思路。

 


 

首先是第一個優化例子:

 1 public abstract class Animal {
 2   public abstract string Speak();
 3 }
 4  
 5 public class Cow : Animal {
 6    public override string Speak() {
 7        return "Moo";
 8    }
 9 }
10  
11 public class Pig : Animal {
12     public override string Speak() {
13         return "Oink";
14    }
15 }
16 
17 public class Farm: MonoBehaviour {
18    void Start () {
19        Animal[] animals = new Animal[] {new Cow(), new Pig()};
20        foreach (var animal in animals)
21            Debug.LogFormat("Some animal says '{0}'", animal.Speak());
22  
23        var cow = new Cow();
24        Debug.LogFormat("The cow says '{0}'", cow.Speak());
25    }
26 }

 

這個是最教條主義的面向對象編程入門示例,很顯然,從常識來思考的話,示例中的animal.Speak()是多態的,而cow.Speak()不是,前者會做一次virtual function call,而後者會做一次direct function call,兩者的性能差距是一次虛函數表查詢。

但是,IL2CPP實際上並不會這麼做。IL2CPP的優化策略非常保守,而且為了實現簡單,IL2CPP並不會在讀IL指令的時候維護上下文狀態。因此IL2CPP看到cow.Speak()沒有辦法判斷cow的具體類型,保險起見,只能做一次虛函數表查詢,也就是表現為virtual function call。

 

當然優化起來也很簡單,程式員人肉加hint即可。而且這種hint方式我們在各種語言里都能見到,那就是給Cow的類型定義加一個sealed修飾符,問題終結。

 


 

優化一方面要跳過不需要的邏輯,另一方面還要簡化無法跳過的邏輯。畢竟對於大多數情況,virtual function call的開銷是逃不掉的。接下來,IL2CPP開發組又介紹了他們優化virtual function call的思路。

 

先看示例代碼:

 1 class BaseClass {
 2    public virtual string SayHello() {
 3        return "Hello from base!";
 4    }
 5 }
 6 
 7 class GenericDerivedClass<T> : BaseClass {
 8    public override string SayHello() {
 9        return "Hello from derived!";
10    }
11 }
12 
13 public class VirtualInvokeExample : MonoBehaviour {
14    void Start () {
15        Debug.Log(MakeRuntimeBaseClass().SayHello());
16    }
17  
18    private BaseClass MakeRuntimeBaseClass() {
19        var derivedType = typeof(GenericDerivedClass<>).MakeGenericType(typeof(int));
20        return (BaseClass)FormatterServices.GetUninitializedObject(derivedType);
21    }
22 }

MakeRuntimeBaseClass().SayHello()這個坑相信大家剛接觸Unity的時候都踩過,由於iOS平臺不支持JIT compile method,這裡如果不做hint,就會導致真機運行時crash。

IL2CPP的runtime library實現也類似,會在SayHello這個virtual function call的過程中查一次虛表,如果找不到調用方法,就會拋出一個托管的異常。

 

代碼在這裡:

1 static inline void GetVirtualInvokeData(Il2CppMethodSlot slot, void* obj, VirtualInvokeData* invokeData) {
2    *invokeData = ((Il2CppObject*)obj)->klass->vtable[slot];
3    if (!invokeData->methodPtr)
4        RaiseExecutionEngineException(invokeData->method);
5 }

這裡對於我們寫邏輯的來說,其實真沒什麼可優化了。而且對於有指令級優化經驗的程式員,會把這個機會交給CPU的branch prediction。

但是IL2CPP團隊還是選擇把這個if優化掉了。簡單地說就是自己寫了個stub method,然後vtable[slot]本來應該為null的情況都給指到stub method。

這樣,雖然在極少數需要拋出異常的情況下,多了一次函數調用的開銷,但是對於絕大多數情況,都省了一次if檢查開銷。

按IL2CPP官博的說法是,這個優化提高了3%到4%的表現,我們就姑且信之,淆習一個。

 


 

接下來是原博的第三個示例:

 1 interface HasSize {
 2    int CalculateSize();
 3 }
 4  
 5 struct Tree : HasSize {
 6    private int years;
 7    public Tree(int age) {
 8        years = age;
 9    }
10  
11    public int CalculateSize() {
12        return years*3;
13    }
14 }
15 
16 public static int TotalSize<T>(params T[] things) where T : HasSize
17 {
18    var total = 0;
19    for (var i = 0; i < things.Length; ++i)
20        if (things[i] != null)
21            total += things[i].CalculateSize();
22    return total;
23 }

註意第21行中的things[i] != null,這裡如果T具現為Tree類型,就會做一次裝箱操作。

如果對代碼生成有瞭解的同學,可能還會聯想到generic sharing,也就是泛型函數具現為不同的引用類型時可以共用同一個方法實例,而具現為值類型時就會決議到不同的方法實例。

同時由於IL2CPP的AOT性質,編譯期就已經知道了這些事情,所以IL2CPP完全可以把具現的每個值類型泛型函數實例特殊處理,去掉裡面的裝箱操作。

 

事實上,IL2CPP就是這麼乾的,也確實讓程式員少操了不少心。

 


 

小結一下,以上優化技巧,我們應該如何在寫邏輯的時候應用上?下麵就逐條淆習一下:

  • 第一個例子中,IL2CPP藉助編譯期hint獲得了額外的優化元信息。

針對這一點不太好列舉寫邏輯時候的應用情景,如果經常用可以給類型加註記或Attribute的語言(比如C#)可能會有類似的優化經驗。

假設我們要開發一個非侵入式的序列化庫,核心需求是把傳進來的object序列化成位元組流。

對於庫來說,傳進來的是一個未知的object,需要藉助反射拿到類型元信息,然後動態生成序列化代碼,以供之後的該類型object序列化使用。

這就跟JIT一樣,相當於在每種類型的object第一次序列化的時候,庫需要動態生成方法,這個成本相當高,不過好在可以之後攤還。但是對於有些服務端來說,這種隨機的性能壓力是不可忍受的。

因此我們可以hint住可能會序列化的類型定義,形成一種約束,規定程式員在運行時只能給庫這些hint過的類型的object。

這樣,序列化庫初始化的時候一次性生成好這些類型的序列化函數,就能把不確定的消耗轉化為確定的消耗,把運行時的消耗提前,提高整體的性能表現。

 

  • 第二個例子中,IL2CPP把nullcheck的極少數分支轉為stub method,消除了nullcheck。

其實我們在寫邏輯的時候,也不知不覺就會寫出各種帶if-elseif的噁心邏輯,這時候我們也可以用類似於stub method/stub class的方法,既能讓代碼變優雅,又能提高效率。

舉個例子,我們有一個IServiceProvider,它會根據配置的不同實例化為不同的ServiceProvider。那麼,一種設計是每個用到ServiceProvider的地方都checknull,另一種設計是讓ServiceProvider一開始初始化為一個TrivialServiceProvider,後面該怎麼用就怎麼用。

其實兩種設計並沒有絕對的好壞之分,完全看IServiceProvider在邏輯中扮演什麼角色。

如果IServiceProvider的介面並不具有預設值語義,那有可能第一種設計更適合你。但是相反的話,第二種比第一種更優雅,而且對於trivial占極少數情況的邏輯,還能獲得額外的性能表現。

 

  • 第三個例子中,IL2CPP對可以優化的情況做了特殊處理。

這類例子就比較多了,比如redis的zset在元素少的時候會用ziplist,元素多的時候才改為skiplist等等。

 

最近開始在訂閱號寫文章了,覺得合適的會轉過來博客。但是幾番對比,發現訂閱號的寫文章體驗完爆各種博客。

有興趣的同學可以關註下訂閱號「說給開發游戲的你」,下麵是二維碼。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • Calculate the sum of two integers a and b, but you are not allowed to use the operator + and -. Example:Given a = 1 and b = 2, return 3. 個人思路:繞開+、-,利用 ...
  • 一、標識符 二、中置操作符 中置表達式,操作符位於兩個參數之間 1 to 10 1.to(10) 1 -> 10 1.->(10) 三、一元操作符 a.標識符() 1 toString 1.toString() +、-、!、~ 可以作為前置操作符,轉換成名為 unary_操作符 的方法調用 -a 和 ...
  • 表單的數據檢驗對一個程式來講非常重要,因為對於客戶端的數據不能完全信任,常規的檢驗類型有: 參數為空,根據不同的業務規定要求表單項是必填項 參數值的有效性,比如產品的價格,一定不能是負數 多個表單項組合檢驗,比如在註冊時密碼與確認密碼必須相同 參數值的數據範圍,常見的是一些狀態值,或者叫枚舉值,如果 ...
  • 一、生成文件夾。 mkdir();--新建目錄 參數:pathname:目錄的路徑。 mode:預設的 mode 是 0777,意味著最大可能的訪問權。有關 mode 的更多信息請閱讀 chmod() 頁面。 看到上面的函數了嗎?記牢。上節課沈老師留了一個作業,讀取god.json文件,生成一個最簡 ...
  • 從控制台輸入輸出,來進行資料庫的插入和查詢操作的小程式(利用JDBC) ...
  • 一、Redis基礎介紹 redis是一個key-value存儲系統。和Memcached類似,它支持存儲的value類型相對更多,包括string(字元串)、list(鏈表)、set(集合)、zset(sorted set --有序集合)和hash(哈希類型)。這些數據類型都支持push/pop、a ...
  • 在寫C++程式中,總會遇到要從一個字元串中查找一小段子字元串的情況,對於在C中,我們經常用到strstr()或者strchr()這兩種方法。而對於C++的string,我們往往會用到find()。 C++:#inlcude<string>C: #include<string.h>find():在一個 ...
  • 一、spring xml配置(不包括AOP,主要瞭解在初始化及實例化過程中spring配置文件中每項內容的具體實現過程,從根本上掌握spring) 二、BeanFactory容器後置處理介面(BeanFactoryPostProcessor) 三、基於註解的依賴註入 四、aop部分 五、spring ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...