跟Unity3D學代碼優化

来源: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
更多相關文章
  • LZO說明 摘要 LZO 是一個用 ANSI C 語言編寫的無損壓縮庫。他能夠提供非常快速的壓縮和解壓功能。解壓並不需要記憶體的支持。即使使用非常大的壓縮比例進行緩慢壓縮出的數據,依然能夠非常快速的解壓。LZO 遵循 GNU 的 GPL 使用許可。 介紹 LZO 非常適合進行數據的實時壓縮解壓處理,這 ...
  • 話不多說,直接進入主題。 需求:基於Http請求接收Json格式數據,返回Json格式的數據。 整理:對接收的數據與返回數據進行統一的封裝整理,方便處理接收與返回數據,並對數據進行驗證,通過C#的特性對token進行驗證,並通過時間戳的方式統一處理接收與返回的時間格式。 請求Json格式: 返回Js ...
  • Atitit. null錯誤的設計 使用Optional來處理null 然後,我們再看看null還會引入什麼問題。 看看下麵這個代碼: String address = person.getCountry().getProvince().getCity(); 如果你玩過一些函數式語言(Haskell ...
  • 知乎上有一種說法是「編譯器、圖形學、操作系統是程式員的三大浪漫」。   先不管這個說法是對是錯,我們假設一個程式員在國內互聯網公司寫代碼,業餘時間不看相關書籍。那麼三年之後,他的這些知識會比在校時損耗多少? 很顯然,損耗的比例肯定非常高,畢竟國內互聯網公司日常開發工作中,程式員基本很少接觸... ...
  • 緣起termtalk 一切起源於我對蘑菇街termtalk開源IM系統源代碼的好奇,termtalk簡稱tt。無論如何,都應該先向tt致敬,開源實屬不易。看了一些分析tt架構的文章,感覺還不錯,說是能支持高併發高可用的。聽說有一些公司也借用了該開源代碼做產品,那tt應該還是不錯的。但是正等我去打開t ...
  • 高可用架構Keywords 分層解耦 交易系統緩存 分區一致性 資源隔離重點保障 某移動高可用架構 分渠道資源隔離部署 簡訊渠道業務處理機制 -------------------------------------------------------------------------------... ...
  • Atitit.提升 升級類庫框架後的api代碼相容性設計指南 1. 增加api直接增加,版本號在註釋上面增加1 2. 廢棄api,使用主見@dep1 3. 修改api,1 4. 修改依賴import,雅瑤增加文件模式。保持相容性。。1 5. 優先選擇同一個文件內的修改,因為文件多了不好管理了,編譯速 ...
  • Java無疑是最成功的項目之一了,而在其中學習設計模式和架構設計,無疑是最好不過了。 概念: 提供一種方法訪問容器中的各個元素,而又不暴露該對象的內部細節。 使用場景: 和容器經常在一起,我們定義了一個容器,還要提供外部訪問的方法,迭代器模式無疑是最好不過了。 迭代器模式的UML類圖: 下麵的代碼是 ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...