《圖解 C# 教程 第 5 版》與效能優化(附 Unity 專案)
這本書仍然是入門 C# 最好的一本書。
這本書新版出來的時候我十分關注,於是英子姐送了一本給我,本文也是答應英子姐所寫的一篇文章。她一開始還問我“你現在還需要看這本入門書嗎?”,我認為是的。工作了遇到了不少問題,大都跟自己基礎不牢有關係。
這本書以圖形為載體,生動地介紹了 C# 語言本身。其中圖形對我們瞭解 C# 語法在記憶體中的本質十分有幫助,非同步、異常等章節中的處理流程圖也很清晰明瞭,這也是我看重的一點。
在記憶體中的形態
為什麼要了解 C# 在記憶體中的形態呢?
書中第四章介紹記憶體區域的棧中,有一句話說的很好:
作為程式設計師,你不需要顯式地對它做任何事情。但瞭解棧的基本功能可以更好地瞭解程式在執行時在做什麼,並能更好地瞭解 C# 文件和著作。
遊戲開發中,除了業務邏輯,我們還會更關注遊戲的效能本身。我們需要保證遊戲能流暢執行在大部分機型上,保證每一幀能流暢地播放,例如 CPU 需要處理渲染程式碼、物理模擬、動畫回撥等等,其中我們的程式碼也有可能引起效能問題。我們需要更瞭解執行程式碼的代價,例如:
- 這些程式碼產生了多少 GC
- GC 只會產生一次還是每幀都會產生
- 在極端情況下程式碼的效能如何
- 是否使用了正確的資料結構
- Unity API 或者一些庫 API 的背後到底做了什麼
- ...
這本書相對上一版多了 .Net Core, C# 7.0 語法的講解,對於我而言,重溫的是第 4、7、11、13、15、17、19 和 27 章節,這些內容是我工作中經常要接觸、著手優化的地方。書中對於非同步程式設計也介紹地很好,但對於我來說,反射、非同步程式設計、新增語法等到以後有需要再看也不遲。
指令碼的效能優化,無非是用更合適的程式碼去實現需求,不必要的記憶體都給我吐出來!(注:作者在生活中並沒有這麼吝嗇)
下面會列舉一些程式碼寫法的效能對比。
結構和類
這其實也是用棧還是用堆的考量。
垃圾回收
Unity 用的是 mono 虛擬機器,其堆的記憶體是通過垃圾回收演算法 Boehm GC 來管理的,其不分代(Non-generational)和非壓縮式(Non-compacting)的特性,導致了我們平常要注意避免載入過多的小記憶體,從而記憶體碎片化(Memory fragmentation)。
分代:大塊記憶體、小記憶體、超小記憶體分在不同記憶體區域來進行管理。此外還有長久記憶體,當有一個記憶體很久沒動的時候會移到長久記憶體區域中,從而省出記憶體給更頻繁分配的記憶體。
壓縮式:當有記憶體被回收的時候,壓縮記憶體會把下圖空的地方重新排布。
記憶體碎片化:記憶體過多小記憶體,導致大記憶體不能有效地被使用。
具體可以參考 Unity 文件 Understanding the managed heap
同時也推薦高川老師的演講:淺談 Unity 記憶體管理,和我看視訊時的筆記:筆記。
用結構還是類
這裡推一篇微軟官方文件:Choosing Between Class and Struct
引用型別被分配在堆上並被垃圾回收演算法管理,值型別則分配在棧上,棧會按需 unwind 來釋放他們。因此,值型別的釋放比引用型別的釋放開銷要小。
書中 11.9 小節還提到:
對結構進行分配的開銷比建立類例項小,所以使用結構代替類有時可以提高效能,但要注意裝箱和拆箱的高昂代價。
值型別陣列的分配和釋放比引用型別陣列的分配和釋放開銷也更小。
除了最基本的修改值型別和引用型別的區別外,要注意的是傳遞引數或者返回返回值的時候,值型別都會隱性地被建立,這可能也會產生沒想到的記憶體開銷。
從 .Net 記憶體分配成本的角度來說,類的物件儲存的記憶體首先需要分配 4 個位元組作為物件頭位元組(object header word),跟著再分配 4 個位元組作為方法表指標(method table pointer),這些欄位是服務於 JIT 和 CIL 的,是隱藏的分配成本。
保留在堆中所需的記憶體還會根據作業系統位數來決定:
- 32 位系統中,堆上的物件會對齊到最近 4 位元組的倍數,因此如果一個物件只有一個 byte 成員,也需要對齊佔 4 個位元組,因此這個物件總共佔堆上 12 個位元組。
- 64 位系統中,堆上的物件會對齊到最近 8 位元組的倍數,方法表指標和物件頭位元組也會分別佔 8 位元組的記憶體。
(注:平常開發我們不需要這麼摳門,上面只是一個小知識點。)
大多時候我們都會用型別來實現設計模式、框架的設計,那什麼時候使用結構體呢?我們可以遵循微軟爸爸的建議:
- 邏輯上表示單個值
- 大小小於 16 個位元組
- 不會改變值
- 不需要經常裝箱拆箱
對於一些特定的場景下,我們也可以享受值型別陣列在記憶體中線性排布的福利,例如記憶體連續、SIMD 等。Unity 的 DOTS 技術棧就是一個很好的例子。
推薦閱讀:
- 在C#中使用Struct代替Class
- 作者用 DOTS(結構體、Job System、Burst)實現 A 星尋路實現效能飛躍:y2b 搜 “Pathfinding in Unity DOTS!”
裝箱
第 17 章介紹了轉換,其中提到了裝箱拆箱。那麼裝箱的代價有多大呢?我們可以做個測試:
public const int Iterations = 100_000;
// 其他地方初始化了 Iterations 大小的隨機陣列
private int[] numberArr = null;
protected override bool MeasureTestA()
{
// 設定大小以避免自動擴容帶來的效能消耗
Stack stack = new Stack(Iterations);
for (int i = 0; i < Iterations; i++)
{
stack.Push(numberArr[i]); // int -> object 裝箱
}
return true;
}
protected override bool MeasureTestB()
{
Stack<int> genericStack = new Stack<int>(Iterations);
for (int i = 0; i < Iterations; i++)
{
genericStack.Push(numberArr[i]);
}
return true;
}
檢視 Profiler 可以看到,呼叫十次 TestA 產生了 26.7MB GC,用了 267.22 毫秒;呼叫十次 TestB 只產生了 3.8MB GC,用了 20.92 毫秒。因此大量的裝箱拆箱會導致不必要的效能消耗,而有些消耗則是完全可以避免的。
我的 Rider 外掛 Heap Allocations Viewer 也會提示我 TestA 中存在裝箱的情況。
最後
寫到這裡,發現還很多東西沒講完就已經這麼多篇幅了...
大家對於平常程式碼的不同寫法也可以測試下效能,例如:
- foreach 和 for
- 裝箱和拆箱
- 一維陣列、多維陣列(矩形陣列)和交錯陣列(Jagged Arrays)
- 這裡還是強調下儘量使用一維陣列,實在需要用多維陣列的話,可以改用交錯陣列
- 通過 for 迴圈複製陣列和 Array.CopyTo 方法
- 字串拼接,string 和 Stringbuilder
- 反射和 DynamicMethod
- ...
上面裝箱的截圖中的測試專案我也上傳了 Github:Latias94/UnityCsharpPerformanceTest,不用 Unity 的同學也可以參考下實際的測試程式碼:UnityCsharpPerformanceTest/Assets/Scripts,自己寫個命令列專案來跑下對比。
當然我們開發中還是要以需求的變化為主,不能過早優化從而破壞程式碼的擴充套件性。
相關文章
- Unity C# 反射效能優化UnityC#反射優化
- 讀《圖解TCP/IP(第5版)》圖解TCP
- 怎麼圖解效能優化?圖解優化
- XCel 專案總結:Electron 與 Vue 的效能優化Vue優化
- 網站技術架構與效能優化,附高效能思維導圖網站架構優化
- vuejs專案效能優化總結VueJS優化
- Vue 專案效能優化 — 實踐指南Vue優化
- JN專案-地圖定位優化地圖優化
- 前端效能優化 --- 圖片優化前端優化
- Web效能優化:圖片優化Web優化
- AIX 5L 記憶體效能優化,第 3 部分AI記憶體優化
- AIX 5L 記憶體效能優化,第 2 部分:AI記憶體優化
- HTML5 網路拓撲圖效能優化HTML優化
- 資訊系統專案管理師教程(第3版)專案管理
- 曬書 - 《C# 圖解教程(第四版)》C#圖解
- 效能優化04-圖片優化優化
- Web效能優化之圖片優化Web優化
- MySQL5:效能優化MySql優化
- Unity官方文件之“圖形效能優化-幀偵錯程式”的翻譯Unity優化
- .NET(C#)程式碼效能優化C#優化
- iOS 圖形效能優化iOS優化
- iOS圖層效能優化iOS優化
- 【效能優化】直方圖優化直方圖
- 【效能優化】執行計劃與直方圖優化直方圖
- Android效能優化——圖片優化(二)Android優化
- 效能優化詳解優化
- 《圖解效能優化》閱讀感想(無干貨)圖解優化
- CSAPP 5 - 優化程式效能APP優化
- HTML5 電信網路拓撲圖效能優化HTML優化
- C# 程式碼效能優化舉例C#優化
- SQL效能第1篇:關係優化SQL優化
- RedisStack部署/持久化/安全/與C#專案整合Redis持久化C#
- iOS開發——專案實戰總結&UITableView效能優化與卡頓問題iOSUIView優化
- vue-cli3專案搭建配置以及效能優化Vue優化
- 手淘雙十一521 效能優化專案揭祕優化
- Nginx安全優化與效能調優Nginx優化
- Golang效能分析與優化Golang優化
- 前端工程與效能優化前端優化