UE 手游在 iOS 平台运行时内存占用太高?试试这样着手优化
性能優(yōu)化,對(duì)游戲開發(fā)來說是一個(gè)需要不斷鉆研的課題,性能越好,游戲才會(huì)運(yùn)行的更加順暢,玩家的體驗(yàn)感才會(huì)更好。騰訊游戲?qū)W院專家、游戲客戶端開發(fā) Leonn,將和大家分享 UE 手游在 iOS 平臺(tái)上的內(nèi)存分布和優(yōu)化。(本文首發(fā)于騰訊游戲?qū)W院專家團(tuán)月刊《EXP 手冊(cè)》)
文 | Leonn
騰訊游戲?qū)W院專家 游戲客戶端開發(fā)
對(duì)于在 iOS 平臺(tái)上運(yùn)行的 UE 程序,經(jīng)常會(huì)出現(xiàn)內(nèi)存占用較高,xcode 的內(nèi)存統(tǒng)計(jì)和 UE 的統(tǒng)計(jì)有偏差的問題。本文將討論下 iOS 上內(nèi)存的管理機(jī)制,內(nèi)存的組成,iOS 特有的一些資源管理特性,以及 UE 程序針對(duì) iOS 常見的內(nèi)存瓶頸和優(yōu)化。
iOS 程序內(nèi)存分配原理
1.1 iOS 系統(tǒng)內(nèi)存分配原理
iOS 也是一個(gè)類 linux 的系統(tǒng),所以基本的內(nèi)存分配還是走的虛擬內(nèi)存系統(tǒng)中 page mapping 和 swap out 那套,即虛擬內(nèi)存訪問缺頁產(chǎn)生對(duì)物理內(nèi)存的實(shí)際占用,以及對(duì)物理內(nèi)存的喚入喚出(詳細(xì)可以看前面的《Android 內(nèi)存分布和優(yōu)化》的文章)。iOS 上的 page size 為 16k,
Swap out 和 compressed memory
關(guān)于 swap out 機(jī)制,由于移動(dòng)端內(nèi)存的壽命問題,沒有實(shí)現(xiàn)傳統(tǒng)虛擬內(nèi)存那樣的 swap in/out 機(jī)制,只有一些 read-only 的數(shù)據(jù)(例如代碼)在被 swap out 的時(shí)候,會(huì)被直接從物理內(nèi)存移除,而不會(huì) back up 到磁盤文件上,下次 swap in 就直接重新加載,也就是非 read only 的 page 只要被訪問就永遠(yuǎn)不可能喚出物理內(nèi)存。
?
此外 iOS 還有額外的 memory compress 機(jī)制,即將一些不常用的內(nèi)存壓縮存儲(chǔ)在內(nèi)存中。
?
所以 iOS 中有個(gè)特有的表示內(nèi)存占用的說法 memory footprint 就是值得壓縮前的總大小,而不是當(dāng)前的實(shí)際物理內(nèi)存大小。
當(dāng)系統(tǒng)內(nèi)存吃緊的時(shí)候,系統(tǒng)會(huì)通過 didrecievememorywarning 通知程序,讓程序自愿的釋放一些內(nèi)存,這時(shí)候如果程序仍然不能有效降低內(nèi)存,進(jìn)程就會(huì)被殺掉。
VM Object
在 iOS 內(nèi)核中,使用一個(gè)叫做 VM Object 的對(duì)象表示在虛擬內(nèi)存空間的一塊被映射的內(nèi)存區(qū)域(region),一個(gè) region 是由幾個(gè)連續(xù)的 page 組成,所以一個(gè) vm object region 的起始地址必定是某個(gè)虛擬空間上 page 的起始地址。VM Object 還記錄了其他的一些信息,包括繼承關(guān)系,讀寫權(quán)限,是否是 wired(能不能被 swap-out)。此外它還關(guān)聯(lián)了一個(gè) pager,用來做內(nèi)存映射,這個(gè) pager 是 default pager 或者 vnode pager 的一種, default pager 負(fù)責(zé)將虛地址 VA 跟物理地址 PA 做映射(即訪問 va 缺頁后將開辟物理內(nèi)存),vnode pager 直接將文件映射到虛地址空間(這樣不經(jīng)過內(nèi)存直接讀寫文件)。
VM_Copy
Vm object 的 pager 除了可以是 default pager 或者 vnode pager 之外,還可以直接映射另外一個(gè) vm object,這時(shí)是為了做 copy-on-write 優(yōu)化。這允許不同的 vm object 映射同一段 page 區(qū)域,直到其中一個(gè) vm object 需要發(fā)生寫入它才會(huì) copy 出一個(gè)新的。在 iOS 系統(tǒng)下我們可以直接調(diào)用 vm_copy 代替 memcpy 來執(zhí)行這種 copy-on-write 的 copy,只要你 copy 后面不寫入,就一直沒有實(shí)際的 copy 開銷。Vm_copy 的唯一缺點(diǎn)是如果發(fā)生寫入那么 copy 會(huì)存在延時(shí),所以對(duì)于頻繁的小內(nèi)存的 copy 還是推薦直接 memcpy。
?
1.2 iOS 系統(tǒng)內(nèi)存的分配方式
在《Android 內(nèi)存分布和優(yōu)化》中我們講了使用 malloc 和 mmap 分配系統(tǒng)內(nèi)存的原理和區(qū)別。這里我們?cè)敿?xì)討論下他們?cè)?iOS 上的特點(diǎn)。
vm_allocate
首先 iOS 上做 mmap 的函數(shù)是 vm_allocate,它同 Android 上的 mmap 用法相當(dāng),即分配一個(gè)虛存,做物理內(nèi)存或者文件映射。
Malloc
直接從堆上分配內(nèi)存,并且這些內(nèi)存會(huì)立即從虛擬內(nèi)存映射物理內(nèi)存,并且不會(huì)初始化內(nèi)存內(nèi)容。在 iOS 上 malloc 的底層實(shí)現(xiàn)細(xì)節(jié)如下:
小內(nèi)存:小于幾個(gè) pagesize 的內(nèi)存,malloc 會(huì)從一個(gè) pool 上分配,這個(gè) pool 本身是由 vm_allocate 分配的虛存,這些虛存可能都是已經(jīng)存在物理內(nèi)存映射的了,這個(gè)池分配的粒度都是按照 16 字節(jié)對(duì)齊,所以我們用 malloc 也盡量 16 字節(jié)對(duì)齊,否則就存在了浪費(fèi)。這個(gè)小內(nèi)存池的預(yù)分配的大小有多大要取決于系統(tǒng)策略。
大內(nèi)存:對(duì)于大于幾個(gè) pagesize 的內(nèi)存,Malloc 自動(dòng)使用 vm_allocate,它只分配虛存,不立即映射物理內(nèi)存,分配粒度為 1 個(gè) page 大小(即 16k),因?yàn)椴煌?vmobject 是由不同的獨(dú)立的 page 組成,這時(shí)如果 malloc 的大小沒有 16k 對(duì)齊,也會(huì)產(chǎn)生較多的內(nèi)存浪費(fèi)。在這種情況,使用 malloc 和直接使用 vm_allocate 是相當(dāng)?shù)摹?/span>
Calloc
calloc 同 malloc 不同的是,它在分配內(nèi)存后,在使用前會(huì)保證將內(nèi)存初始化為 0。他比用 malloc+memset 要優(yōu)化,因?yàn)?memset 發(fā)生時(shí)會(huì)立即產(chǎn)生缺頁造成物理內(nèi)存占用,然后初始化 0,而 calloc 則是延遲的,它不會(huì)馬上產(chǎn)生物理內(nèi)存占用,而是要等得到真正這塊內(nèi)存被使用之前。我們應(yīng)該完全使用 calloc 代替 malloc+memset。
Malloc zone
iOS 上所有的內(nèi)存分配都是來自于某個(gè) malloc zone 的,每個(gè) zone 有獨(dú)立的內(nèi)存池,默認(rèn)的分配都是在 default malloc zone 上的。使用 malloc zone 有個(gè)好處是減少小內(nèi)存池的浪費(fèi)。我們知道內(nèi)存池的浪費(fèi)主要有兩種來源,一種是對(duì)齊浪費(fèi),即為了匹配內(nèi)存池的分配粒度,沒有對(duì)齊的內(nèi)存產(chǎn)生的浪費(fèi),如 malloc 一個(gè) 17 byte 的內(nèi)存其實(shí)需要 malloc 32byte,另一種則來源于對(duì)頁的空白浪費(fèi),例如在頻繁的分配內(nèi)存后,會(huì)開辟大量的新 page,這樣在后面即使先后發(fā)生了一些釋放,但是因?yàn)獒尫挪患性谝粋€(gè) page 上,也導(dǎo)致了很多 page 上只被少量的 block 占據(jù),導(dǎo)致大量的空白部分的浪費(fèi)。如果我們可以知道某些內(nèi)存的生命周期是相同的,那么我們可以把它們?cè)谕瑯拥囊粋€(gè) zone 上分配,這樣我們?cè)诖_定他們的生命周期全都到期后,可以對(duì)整個(gè) zone 執(zhí)行釋放的操作,這樣就杜絕了這兩種浪費(fèi)。在 iOS 下使用 malloc zone 的相關(guān)接口是:
?
- malloc_create_zone 創(chuàng)建一個(gè) zone
- malloc_zone_malloc 再某個(gè) zone 上分配
- malloc_destroy_zone 釋放整個(gè) zone
UE 程序在 iOS 上的內(nèi)存組成清單
了解了 iOS 上的基本內(nèi)存分配原理后,我們來統(tǒng)計(jì)我們 iOS 上的 UE 程序的內(nèi)存組成。在對(duì) UE 程序進(jìn)行內(nèi)存分析和優(yōu)化過程中,我們要做的的第一件事就是獲取一個(gè)完整的關(guān)于你程序的內(nèi)存組成清單。UE 的引擎內(nèi)部提供了 LLM,memreport 等內(nèi)存統(tǒng)計(jì)工具,但是這些只是 UE 能感知到的內(nèi)存,我們需要能明確整個(gè)程序的內(nèi)存被花到哪里了,以及為什么程序會(huì)因內(nèi)存過高而產(chǎn)生問題。
2.1 iOS 內(nèi)存組成統(tǒng)計(jì)口徑
Memory footprint
Android 上我們一般使用 PSS,即程序(按分?jǐn)偨y(tǒng)計(jì)共享庫)分配的實(shí)際物理內(nèi)存大小來定義內(nèi)存開銷。iOS 有所不同,iOS 上通常使用 memory footprint(下面簡(jiǎn)寫為 mem foot)這一個(gè)概念來定義內(nèi)存開銷,mem foot 同實(shí)際占用的物理內(nèi)存之間有一定差別。Mem foot 在 iOS 上的定義是進(jìn)程實(shí)際占用的物理內(nèi)存+進(jìn)程被壓縮了的內(nèi)存在壓縮前的大小,即 mem foot = resident + swapped (這里的 swapped 不是指 swap out 的意思,是前文說到的 iOS 的內(nèi)存壓縮機(jī)制)。所以從定義上看,所謂 mem foot 是指你的進(jìn)程所可能觸碰到的所有物理內(nèi)存大小(盡管部分已經(jīng)被壓縮),這就是腳印的意思。
在 xcode 的 allocater 中,我們可以計(jì)算 vm tracker 中 all 中的 resident+swapped 的大小來得到 mem foot 值。如果是在代碼中,則可以通過 darwin 內(nèi)核的接口 task_info 獲取 TASK_VM_INFO 來獲取其中的 phys_footprint 來獲得,darwin 源碼中關(guān)于 phys_footprint 的定義是
?
其中 internal 即除了顯存外的 resident 內(nèi)存,internal_compressed 即指除了顯存外的 swapped 部分,iokit_mapped 一般就是(其實(shí)是不能使用 purgeable memory 的)顯存,后面的 purgeable 是指使用 purgeable memory 中屬性為 nontvolatile 的。關(guān)于 purgeable memory 后文再說。
可以說 memery footprint 是 iOS 上統(tǒng)計(jì)內(nèi)存占用的金標(biāo)準(zhǔn)。
XCode Gauge
當(dāng)我們使用 xcode 運(yùn)行游戲,會(huì)看到一個(gè)實(shí)時(shí)的顯示內(nèi)存的儀表盤,如下圖
?
這個(gè)叫做 xcode memory gauge,它統(tǒng)計(jì)的又是什么內(nèi)存呢,其實(shí)它嚴(yán)格來說統(tǒng)計(jì)的不是 memory footprint,它統(tǒng)計(jì)的是 vmtracker 里面 dirty+swapped 的值,那么什么是 dirty 內(nèi)存呢,dirty 是指實(shí)際占用的物理內(nèi)存(resident)中那些一定不能被 swap out 出去的內(nèi)存,前面提到 iOS swap out 機(jī)制時(shí)說,iOS 上只有那些可讀的文件等才能被 swap out,這些能 swap out 的內(nèi)存通常危害不大,在內(nèi)存吃緊的時(shí)候可以部分被系統(tǒng)調(diào)度出物理內(nèi)存,他們一般是各種文件映射,代碼庫,符號(hào)文件等,所以 dirty 才是程序動(dòng)態(tài)分配的需要考慮的內(nèi)存,xcode gause 統(tǒng)計(jì)的是真正用戶能夠決定的內(nèi)存腳印大小。他要比 mem foot 小一些,小了那些代碼和文件的內(nèi)存占用。
2.2 iOS 程序主要內(nèi)存構(gòu)成
我們以 memory footprint 為統(tǒng)計(jì)標(biāo)準(zhǔn)來得到 iOS 的完整內(nèi)存構(gòu)成,最正確方便的方法是使用 xcode 的 allocations 工具,里面有個(gè) vm tracker,vm tracker 就是用來跟蹤程序的每個(gè) vmobject 的,即每個(gè)虛擬內(nèi)存區(qū)域的分配情況。在 vm tracker 里面做一個(gè) snapshots,就可以得到當(dāng)前內(nèi)存分配的一個(gè)快照。
?
其中 All 是指總的分類,all 下面是各種細(xì)類,右面的 resident dirty swapped 分別指實(shí)際物理內(nèi)存,不能被 swap out 出去的物理內(nèi)存,以及被壓縮的內(nèi)存的壓縮前大小,最后面的 virtual size 是虛存大小。我們把 all 中的 resident+swapped 就是總 memory foot print。
下面是占據(jù)大頭的幾個(gè)細(xì)類:
?
- IOKIt 和 IOSurface:通常就是指我們 GPU 需要訪問的內(nèi)存,即顯存
- Performance tool data:是實(shí)際運(yùn)行沒有的,Profile 工具本身內(nèi)存。
- Mappedfile:文件映射,用于讀寫的文件,一般不占用很多 dirty
- __LINKEDIT 和 __TEXT:代碼段部分,即代碼段內(nèi)存,只讀,他們一般不占 dirty
- __DATA:代碼的數(shù)據(jù)段,包括可讀寫的全局變量等。
Malloc_NANO/TINY/SMALL/LARGE :這就是前面提到的 iOS 的 malloc 小內(nèi)存池,nano/TINY 指的就是文章最前提到的 malloc zone。雖然默認(rèn)的 malloc 是在 default zone 上分配的,但是系統(tǒng)還是會(huì)根據(jù)不同的大小再選擇不同的 zone。對(duì)于 0-256B 的 malloc,系統(tǒng)會(huì)使用 nano zone,nano zone 比較特殊,它專門為小內(nèi)存而優(yōu)化,并且預(yù)先就 vm_allocate 了一塊 512M 的虛擬內(nèi)存空間做這個(gè) nano zone 的 pool,這塊空間處于堆底。對(duì)于更大一些的小內(nèi)存分配,則會(huì)根據(jù)情況使用到 tiny small large 這三個(gè) zone 的 pool。所以我們推薦大家在 iOS 上對(duì)于 256b 以內(nèi)的內(nèi)存分配直接走 malloc,而不是 UE 的 malloc,可能會(huì)得到更多的收益。
VM_ALLOCATE:這個(gè)就是通過 vm_allocate 方法申請(qǐng)?zhí)摯婧笕表撚|發(fā)的內(nèi)存占用,QQ號(hào)碼轉(zhuǎn)讓平臺(tái)在 UE 里面一定是大頭,因?yàn)?UE 自己的 Fmalloc 在 iOS 上就是走的 vm_allocate。又因?yàn)?UE 的 fmalloc 在做 vm_allocate 的時(shí)候傳遞的 tag 是 255,所以在 vmtracker 中,所有體現(xiàn)為 memory tag 255 的 vm 就是 UE 的 fmalloc。
2.3 UE 程序內(nèi)存組成清單
從 vm tracker 出發(fā),在配合我們?cè)凇禔ndroid 內(nèi)存分布和優(yōu)化》中提到的 UE 的自帶的 LLM 機(jī)制,我們就可以構(gòu)建 UE 程序在 iOS 平臺(tái)上的內(nèi)存完整清單了。它至少應(yīng)該被分割成以下幾個(gè)大部分:
這是我們對(duì)于任何一個(gè) UE 程序,可以得到的在 iOS 上的詳細(xì)的內(nèi)存分布情況,這里面有幾個(gè)問題需要注意:
實(shí)際的總 mem foot 和下面各自項(xiàng)加起來可能是存在一定偏差的。因?yàn)?LLm 中各個(gè)從 UE Fmalloc 出來的子項(xiàng)的總和其實(shí)也只是個(gè) vm_allocate 的虛存大小,它實(shí)際上占用的物理內(nèi)存腳印是要小一些的,另外 LLM 里面對(duì) metal texture 和 buffer 的內(nèi)存計(jì)算也是估計(jì)的,但是一般情況不會(huì)差別過大,我們只要了解這其中存在差值即可。
2.4 顯存大小的統(tǒng)計(jì)
Llm 統(tǒng)計(jì)的 metal tex 和 metal buffer 是 UE 統(tǒng)計(jì)的 gpu 訪問的資源量,它同實(shí)際值是有偏差的,比如 UE 未考慮到使用 purgeable memory,memryless 等資源的內(nèi)存減少,此外顯存上還有除了 tex 和 buffer 之外的其他資源。所以如果想確定真實(shí)的 gpu 資源使用還是要看 IOKIT 的值,只是我們可以用 metal tex 和 buffer 估計(jì)下大致的比例。另外在最新版本 xcode 的截幀工具中我們也可以看到一個(gè)細(xì)節(jié)的 tex 和 buffer 的顯存,如下:、
?
它可以顯示詳細(xì)的 tex 和 buffer 使用情況,但是內(nèi)存值是明顯偏大的,因?yàn)檫@里顯示的是虛存值,不是物理內(nèi)存,所以也只能參考。
UE 程序在 iOS 上的主要瓶頸和優(yōu)化
我們從上面的清單上找到一些內(nèi)存的大頭。在一個(gè)大型 3D 項(xiàng)目中,內(nèi)存較大的塊一般集中在在代碼段部分,GPU 訪問內(nèi)存,Uobject,和 Fmalloc 內(nèi)存池浪費(fèi)上。本章節(jié)也著重講這幾塊的針對(duì) iOS 的常用優(yōu)化方法。很多平臺(tái)通用的優(yōu)化方法在文章《Android 內(nèi)存分布和優(yōu)化》中已經(jīng)說到了,這里就不重復(fù),主要將針對(duì) iOS 平臺(tái)的優(yōu)化手段。
3.1 GPU 訪問內(nèi)存
也可以稱為顯存,顯存的主要組成部分包括 buffer, texture 和 shader。顯存的資源維護(hù)在 iOS 上就有一些特有的優(yōu)化手段。
Purgeable memory
iOS 上的顯存資源 MTLResource(mtlbuffer,mtltexture)使用的都是 purgeable memory。所謂 purgeable memory 是指這種內(nèi)存有三種 purgeable state,分別為 volatile,none-volatile 和 empty。
Volatile:該內(nèi)存資源是暫時(shí)不被使用的,系統(tǒng)將在內(nèi)存吃緊的時(shí)候回收掉它,使用這種類型資源前要查詢?cè)撡Y源是否已經(jīng)無效了(變成 empty 狀態(tài))。
Non_volatile:該內(nèi)存資源一直有用,不能被回收。
Empty:該內(nèi)存資源明確不用了,需要立即釋放。
重要的一點(diǎn)是 volatile 和 empty 狀態(tài)的資源不計(jì)入程序自己的 mem footprint,它算系統(tǒng)的 cache 內(nèi)存。
通過 purgeable state iOS 系統(tǒng)等于為我們提供了一層 pool 或 cache 機(jī)制,我們應(yīng)該盡量利用它。事實(shí)上理想情況我們應(yīng)該把大部分程序用到的可反復(fù)創(chuàng)建的顯存資源用 purgeable state 來管理。就像一個(gè)緩存池一樣,我們不用這個(gè)資源就把他標(biāo)記為 volatile 的,我們想用就從池拿出來,判斷它是否為 empty,被釋放了就重新創(chuàng)建否則直接用。iOS 也開辟了大片的內(nèi)存為這個(gè) purgeable 的資源池,除非我們需要考慮重新創(chuàng)建的成本,否則你的顯存資源都應(yīng)該是在不用后做成 volatile 的。
在 UE 程序中,我們基本會(huì)用到 texure streaming pool 去做 texure 的 streaming,用 mesh streaming pool 去做 mesh 的 streaming,還有各種 rt pool 等等,事實(shí)上這些 pool 里所有的資源都應(yīng)該走 volatile 的機(jī)制。這對(duì)顯存總量的節(jié)省是巨大的,并且更加科學(xué),iOS 系統(tǒng)會(huì)自動(dòng)在內(nèi)存壓力下幫我釋放 cache。
?
Memoryless Resource
除了 purgeable state 之外,metal 的 resource 還可以指定它的 storage mode。Storage mode 用于指定 mtl 資源的被 cpu 和 gpu 訪問的途徑和存儲(chǔ)優(yōu)化。對(duì)于用于做 rt 的 texture,有一個(gè)特殊的存儲(chǔ)模式叫做 memoryless。
我們知道對(duì)于 tbdr 的設(shè)備,我們?cè)趧?chuàng)建一個(gè) rt 之后,rt 的真正訪問是在 gpu 的 cache 上的,除非我們顯示的需要讀取它才需要把整個(gè)資源從 gpu cache resolve 到 memory 上的,所以在很多情況,我們是根本不需要存在一個(gè) memory 上的那一份 rt 的。例如你的只用作深度測(cè)試的深度圖。這樣的資源在 iOS 上可以聲明為 memoryless 的 storage mode,這樣整個(gè) mtltexture 對(duì)象在創(chuàng)建后其實(shí)并不會(huì)產(chǎn)生一個(gè) memory 上的內(nèi)存占用,只會(huì)在 gpu 的 cache 上產(chǎn)生臨時(shí)的對(duì)象,并且用后也不會(huì) resolve 回內(nèi)存,相當(dāng)于我們節(jié)省了整個(gè) rt 的內(nèi)存開銷。
?
在 iOS 上對(duì)于所有的不需要 resolve 的 rt(或 storeaction 設(shè)置為 don’t care)都應(yīng)該設(shè)置為 memoryless。注意如果聲明了 meoryless 但是實(shí)際又去讀取了它則會(huì)產(chǎn)生 crash。
Memoryless 的資源同樣不計(jì)入 mem foot。
Metal resource heap
iOS 中 xture 和 buffer 等資源通常可以直接從 mtldevice 上創(chuàng)建。但是能帶來更多內(nèi)存優(yōu)化的方法是從 mtlheap 上創(chuàng)建。
Metal Resouce Heap 是一個(gè)抽象的用于創(chuàng)建 GPU 資源的 heap,它其實(shí)是維護(hù)了一個(gè)內(nèi)存池。我們可以從 1 個(gè) MTLHeap 上 subllocate 多個(gè) texure 或者 buffer,這樣做有很多好處:
首先它減輕了資源創(chuàng)建的時(shí)間開銷,因?yàn)?heap 的后面是一個(gè)可復(fù)用的內(nèi)存池。
然后因?yàn)?mtlheap 的內(nèi)存可能被系統(tǒng)動(dòng)態(tài)的壓縮不常用的區(qū)域,所以基于 mtlheap 可以減少內(nèi)存占用。
一個(gè) mtlheap 上的 subllocate 資源擁有相同的 storage mode 和 cpu cache mode。另外 mtlheap 需要整體設(shè)置 purgeable state,而不能每個(gè)資源單獨(dú)設(shè)置。所以實(shí)際使用中我們也要有很多的 mtlheap 組成的池,那些 storage mode 相同,purgeable state 相同的資源從一個(gè) heap 上分配,另外 metal 文檔提到單個(gè) heap 也不能過大,因?yàn)閷?duì) heap 的壓縮將影響其性能。
另外 mtlheap 上分配的資源支持 alias 機(jī)制,下面一段會(huì)講。
Resource Alias
從 mtlheap 上創(chuàng)建的資源支持 alias 機(jī)制,alias 也是 iOS 上對(duì) gpu 資源的一個(gè)獨(dú)有優(yōu)化機(jī)制,它是指一個(gè)被標(biāo)記為 alias 的資源 A 可以被 mtlheap 重用,只要重用的資源 B 同 A 有相同的資源格式,只是內(nèi)容不一致,并且邏輯上要保證 A 和 B 不會(huì)被 GPU 同時(shí)使用。
一個(gè)典型的使用場(chǎng)合是后處理鏈,在這里面要涉及很多后處理階段,每個(gè)后處理階段用到不同的 rt,但是這些 rt 不會(huì)被同時(shí)使用,我們就可以把這 rt 做成 alias 的,然后在后面階段不斷被重用,但是分配的內(nèi)存一直都是那一個(gè)。這個(gè)過程要注意使用 fence 或 event 來保證共享這塊內(nèi)存的 rt 不會(huì)被同時(shí)使用到,這里我們的做法應(yīng)該是從 mtlheap 創(chuàng)建一個(gè) rt,然后執(zhí)行第一步后處理,然后插入一個(gè) fence,調(diào)用它的 makealiasable,然后再創(chuàng)建第二個(gè) rt,執(zhí)行第二步后處理…依次下去。
iOS 通過 alias 為我們?cè)诒3诌壿媽佑卸鄠€(gè)資源的同時(shí),做到了一個(gè)底層的內(nèi)存共享。
關(guān)于 shader
Shader 也會(huì)占用較多的顯存。除了常規(guī)的減少 shader 變體之外,我們還應(yīng)該利用 metal 的 shaderlibrary 的預(yù)編譯,預(yù)先將 metal shader 編譯成 native 的 mtllibrary,運(yùn)行時(shí)從 library 中加載 shader function,而不是動(dòng)態(tài)從源碼編譯 shader 。
首先從 mtllibrary 加載要更快,另外 mtllibrary 本身不占用物理內(nèi)存,只占用虛存,只會(huì)在我們用到哪些 shader 的時(shí)候才產(chǎn)生內(nèi)存占用,且由于 native code 本身體積也很小,占用內(nèi)存少。而動(dòng)態(tài)編譯 shader 會(huì)將源碼載入內(nèi)存進(jìn)行便于,源碼體積大本身就會(huì)產(chǎn)生更大的內(nèi)存腳印。
3.2 UE 的 fmalloc
對(duì)于 fmalloc 的分配,除了常規(guī)的減少內(nèi)存分配次數(shù),盡量對(duì)齊內(nèi)存,減少 traray 的 resize,用 inline allocater 等棧模擬等方法之外,在 iOS 上還有一些額外可以嘗試的操作。
UE 使用一個(gè)自帶的內(nèi)存池(Fmalloc)去進(jìn)行內(nèi)存的分配釋放管理,預(yù)先分配整段內(nèi)存,避免 malloc 產(chǎn)生磁盤碎片,這個(gè)過程會(huì)產(chǎn)生前面提到的所有內(nèi)存池都會(huì)有的對(duì)齊浪費(fèi)和頁空白浪費(fèi)。這部分浪費(fèi)的內(nèi)存顯示在了 LLm 的 malloc unused 項(xiàng)目里。
其實(shí)對(duì)于 iOS 來說,iOS 底層已經(jīng)實(shí)現(xiàn)了類似的 malloc 小內(nèi)存池,所以在項(xiàng)目里可以實(shí)驗(yàn)一下在 iOS 上不采用 UE 的 fmalloc 而直接用 malloc 交給 iOS 的內(nèi)存池管理,對(duì)比下內(nèi)存用量的區(qū)別,對(duì)于不同的項(xiàng)目這個(gè)哪個(gè)更好不好說,但是可以試一下。即使最終發(fā)現(xiàn)使用 UE 的 fmalloc 還是更優(yōu)的話,還是可以試一下對(duì)于 256B 以內(nèi)的小內(nèi)存直接使用 iOS 的 malloc 對(duì)比測(cè)試一下,因?yàn)?iOS 的 nano malloc 對(duì)于小內(nèi)存還做了額外的優(yōu)化。
Vm_copy
前面提到了 iOS 上使用 vm_copy 來明確的使用 copy on write 優(yōu)化,所以我們應(yīng)該在代碼里大量的對(duì)于大塊內(nèi)存的 memcpy 換成 vm_copy,除非你發(fā)現(xiàn)這里的 copy 時(shí)間是個(gè)瓶頸。這種優(yōu)化對(duì)于圖形程序的收益是很大的,一個(gè)典型的場(chǎng)景,我們從文件加載模型數(shù)據(jù),然后將其 copy 到申請(qǐng)的一個(gè) mtlbuffer 上,在 copy 之后我們幾乎不會(huì)對(duì)這個(gè)內(nèi)存做更改,如果使用 vm_copy,這個(gè) copy 的操作就剩下了,也減少了內(nèi)存腳印。
3.3 其他
代碼段內(nèi)存
另外對(duì)于 iOS 程序來說,代碼段本身也有可能是個(gè)大頭,這部分可以被 swap out,所以當(dāng)內(nèi)存真正吃緊的時(shí)候危害相對(duì)沒這么大,但還是可以想辦法減小。包括減少代碼體積,減少模板的使用,strip 掉調(diào)試符號(hào),將一些 iOS 上不會(huì)用到的 UE 的 plugin 去掉等。
對(duì) iOS 系統(tǒng) lowmemorywarning 的響應(yīng)
iOS 系統(tǒng)會(huì)根據(jù)不同的機(jī)型制定該機(jī)型內(nèi)存告警的級(jí)別,如 xcode memory gauge 上面顯示的一樣,到達(dá)紅色區(qū)域(如 iphonexr 到達(dá) 2.1G)就會(huì)觸發(fā)內(nèi)存告警,你的程序不能無視內(nèi)存告警,如果在內(nèi)存告警到達(dá)時(shí)不能有效的盡快減輕內(nèi)存負(fù)擔(dān),系統(tǒng)將會(huì)很快結(jié)束該進(jìn)程以回收內(nèi)存。我們需要在收到 didreceivememorywarning 的時(shí)候額外釋放大量任何可以釋放但不會(huì)導(dǎo)致程序異常的內(nèi)存,例如你的各種 cache,這也可以大大減少系統(tǒng)異常退出的幾率。
iOS 內(nèi)存問題排查常用工具
UE 自帶的 memoryreport
這是最簡(jiǎn)單方便的估計(jì) UE 主要資源的方法,里面集成了一些指令,可以看到常用資源如貼圖,uobject 等的詳細(xì)清單,內(nèi)存,但是這里面的內(nèi)存值都是估計(jì)的,只供參考。
UE 自帶的 obj list 指令
用于列出任意 uobject 類型的實(shí)例清單,對(duì)于內(nèi)存泄露和因 uobject 產(chǎn)生的內(nèi)存優(yōu)化很重要。
UE 自帶的 obj refs 指令
用于列出任何 uobject 實(shí)例的引用鏈條,可以在我們通過 obj list 找到泄露的對(duì)象后繼續(xù)追查它未被 GC 的原因。
UE 自帶的 LLM 工具
在《Android 內(nèi)存分析和優(yōu)化》中詳細(xì)講了 UE 這個(gè)工具的實(shí)現(xiàn),它可以將 UE 范疇內(nèi)分配的內(nèi)存按 tag 列出,對(duì)顯存資源也能做出較準(zhǔn)確的估計(jì)。
Xcode 的 allocation
這個(gè)就是平臺(tái)層面的工具,但是也是最全面的,它獲取整個(gè) iOS 程序的內(nèi)存情況,進(jìn)行內(nèi)存分布分析,泄露查找。這里面可以按照各種 tag 列出整個(gè)虛存空間的分配情況,還可以看到如下圖整個(gè)程序的虛存空間每個(gè)地址上的分配情況,tag,大小,映射的物理內(nèi)存大小,類似于 Android 平臺(tái)上的 pmap。
?
還可以插入 generation,來定位在某個(gè)時(shí)間段之內(nèi)增長的內(nèi)存。
注意這個(gè)工具里面的幾個(gè)分類表述,all heap 是指從 malloc 途徑的分配,anonymous VM 是指所有不帶 tag 的 vm_allocate,而 UE 的 fmalloc 因?yàn)閹Я?255 的 tag,所以不在 anonymous VM 分類下,而是在 all vm region 下面的 memory tag 255 下。
?
此外 Xcode 中的 leaker,graph capture 中的 gpu memory,以及 memorygraph 都是比較有用的排查 iOS 內(nèi)存問題和做優(yōu)化的工具。Memory graph 里面就列出了所有程序范疇內(nèi)的 vmobject 分布及之間的關(guān)系。
總的來說,相比較與其他平臺(tái),iOS 是一個(gè)從操作系統(tǒng)層面就極度追求優(yōu)化的一個(gè)平臺(tái),提供了大量平臺(tái)特有的內(nèi)存優(yōu)化手段,這使得同樣的程序可以在 iOS 上比其他平臺(tái)都有少的多的內(nèi)存占用,使及時(shí)對(duì)于大量只有 2G 內(nèi)存的 iOS 設(shè)備仍然能夠良好的體驗(yàn)游戲,而我們不能無視這些手段,需要利用好 iOS 提供給我們的武器去優(yōu)化程序的內(nèi)存使用。
總結(jié)
以上是生活随笔為你收集整理的UE 手游在 iOS 平台运行时内存占用太高?试试这样着手优化的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 在游戏里模拟天空的颜色,太迷人了!
- 下一篇: 《幽灵行动·荒野》中的程序化技术:道路、