Unity遊戲編程中如何避免runtime動態alloc內存?
看技術分享|UNITE課程實錄:Unity項目架構設計與開發管理 47分鐘處提到要避免在runtime時動態地allocate內存,請問這個是什麼意思?大概應如何實現?
比如我有個怪物的gameobject pool,pool初始是空的,runtime當需要用怪A時先看pool里有沒有空閑的A,如果沒有就alloc一個使用,怪A死的時候不dealloc而是加入到pool中,以備下次需要怪A時復用。這個pool本質上是一個dictionary。主講人說的要避免的「動態分配」是否包含「當add新項目時dictionary(或list、數組等,他們在初始分配時若未指定大小都會給一個默認的大小)在初始分配的大小不夠用,dictionary自己進行的再次拓展容量」這個情況?
Unity內存分配有兩部分,Native和Managed。
Managed指C#對象佔用的內存,減少這部分內存分配主要是為了避免GC,因為每次內存分配都伴隨著觸發GC的可能性。其實正常.NET的程序是不需要這麼在意GC的,問題是Unity現在的GC還很渣,因為沒有世代,對象數量多的情況下一次GC掛起的時間很長,會導致遊戲卡頓。
List和Dictionary內部是用數組實現的,數組滿了再加元素需要重新分配數組,這種情況下分配的內存塊一般比較大,幾乎肯定需要觸發一次GC,所以還是一開始指定一個比較大的Capacity比較好。
對GameObject進行Pooling是另外一回事,GameObject和Component這類Unity提供的類型很多都是Native類型的Wrapper,本身的C#對象不佔用什麼內存。重複利用這些對象是為了避免Unity在初始化這些對象時的開銷。比如你Instantiate一個Prefab的時候,並不只是分配一塊內存這麼簡單,實際上是從Prefab進行一次反序列化。如果從非Prefab Instantiate,需要先序列化再反序列化。反序列化完成之後還需要發送Awake消息,如果腳本數量比較多,message本身的overhead就比較大,Awake的初始化邏輯也普遍比較重,可能涉及載入Texture上傳到顯卡這些操作。這些操作都非常吃CPU,會造成比較明顯的卡頓。
解決方法就是把腳本設計成可重複利用的,初始化一次之後,不用了就SetActive(false),放到內存中待機,需要使用的時候換一套數據就可以喚醒。go pool 在unity里的第一價值不是為了減少內存上的動態變化。而是為了避開每次生成和銷毀 go 產生的引擎內部開銷。
至於內存分配的事情,是因為如果有太多內存可回收時,會產生gc的cpu開銷。這個開銷不是常數時間,不注意的話會掉幀。
舉個例子好了
遊戲角色釋放技能或者攻擊行為命中目標。此時需要生成特效實例。如果沒有 pool 就要考慮特效生成時 go 建立的開銷了。這種耗時對遊戲流暢體驗影響是直觀的。
UI 系統每個界面都是獨立的功能,切換的時候大量的載入卸載操作。那又如何?切換界面我又不在意卡不卡個0.1……別在動畫過程中卡就行。完全不需要 pool,gc 就 gc。
一般性能敏感的遊戲邏輯,都是那種能直覺避開蠢數據結構操作的傢伙才能寫出來。所以其實基本上只關心內存總量預算,出了問題找實現者就事論事一下就行了…… gc 從來不是不可解問題
uwa4d 可以把內存優化作為技術服務來做,也正說明了內存這事兒是能獨立解決的。別慌allocate內存不但有可能代價大,而且還有可能被其它線程阻塞。所以對於一些實時性要求高的任務,就需要盡量避免(或者完全不可以)調用預設的內存分配函數。
通常都通過(以各種姿勢使用)內存池來解決。本質上都是預先分配好一塊儘可能大的內存,專供這些高要求的任務使用。答:不是。是為了減少GC。
那段視頻是針對Unity的吧,也就是針對C#的情況。那麼做pool就不是為了減少alloc開銷,而是減少gc開銷。
對native來說的話,在Vista+里,有low-fragment heap,alloc速度遠比xp時代高了很多,幾乎沒必要再自己搞alloc。連原先著名的nedmalloc,用它自己的測試,在Vista+上都能讓malloc超過nedmalloc的速度。如果你的遊戲運行起來流暢的話,可以不用花心思搞 pool manager,現在手機性能都如此強勁了,別在意這些細小的優化了,把時間花在別的地方吧。如果你的遊戲會不停的創建和銷毀大量的物件,那麼是可以考慮避免動態載入而用 pool manager 管理一下,不過還是得看 profiler 的數據,如果確實是動態載入物件造成了性能上的瓶頸。
補充兩點,選擇預分配還有兩個重要原因:
一、降低cachemisses,較多高頻分配和釋放的零碎內存,往往會導致大量cachemisses;
二、transparent_hugepage,連續內存可以有較利用這個特性,降低頁表級數。不過正如前面有人說的,現代內存管理不像以前那麼笨拙,例如glibc,其本身在分配內存時就是先預分配一大塊(128KB),malloc時從中獲取。我覺得可能是因為他語言組織的不太好,導致沒說清楚,我這裡把他說的原畫貼出來
甚至就是說,很多網上的人在討論說,是不是要零容忍動態開闢內存,我們說PoolManager這邊的話有時候動態去load一些東西有時候是沒辦法一定要做的,但是對於其他的我們動態地去開一個數組啊什麼東西,很多的國外的項目組,他們會規定說一定不能這樣干。如果說你要復用,要有一個數組之後,我一開始開20這麼大,始終放在這,有什麼東西同樣的類型塞進去。而不是說我創建20大的數組,然後再destory,然後什麼地方用到再去拿出來。這規則這個思想,我們作為項目的經理或者說一個主程也好,一定要貫徹下去。否則的話,單單因為你這種動態開闢內存,就可能會導致你最後的程序慢的一塌糊塗。
我只能猜測說,題主你舉得例子應該屬於他所說的PoolManager範疇,而他後面說的是啥我是真沒搞清楚,我猜測可能是指使用Grid時一口氣load20個cell,用完就全部destory之類的吧。
推薦閱讀:
※如何在Unity中預覽尋路的結果?
※如何在Unity中對程序進行 Android 真機斷點調試?
※unity 3d 中如何實現以物體的表面作為播放視頻的位置,比如在牆面播放視頻?
※學習 Unity3D 開發,有哪些資源(論壇或網站)?