在Unite Copenhagen 2019上捐康,Unity技術(shù)支持工程師Ignacio Liverotti進(jìn)行了一次主題為《性能優(yōu)化經(jīng)驗(yàn)分享》的演講解总,旨在幫助中級(jí)Unity開發(fā)者診斷和解決遇到的性能問題刻盐。
作為Unity EMEA(歐洲隙疚、中東和非洲)地區(qū)的咨詢及開發(fā)團(tuán)隊(duì)的技術(shù)支持工程師供屉。Ignacio Liverotti在大部分時(shí)間中會(huì)和Unity的大客戶進(jìn)行接觸伶丐,幫助他們解決項(xiàng)目中出現(xiàn)的性能問題哗魂。
本文录别,我們將通過此次演講的內(nèi)容葫男,讓你一窺Unity技術(shù)支持工程師的工作內(nèi)容梢褐,并介紹一些優(yōu)化知識(shí)和技巧盈咳,幫助你應(yīng)用到自己的項(xiàng)目中鱼响。
《性能優(yōu)化經(jīng)驗(yàn)分享》
《性能優(yōu)化經(jīng)驗(yàn)分享》演講的主要內(nèi)容包括:
- Unity技術(shù)支持工程師的簡介和核心工作:項(xiàng)目審查。
- 優(yōu)化和性能分析的介紹桶癣。
- 針對CPU牙寞、GPU和內(nèi)存使用的優(yōu)化,以及介紹解決問題時(shí)用到的工具和方法惹挟。
- 一系列通用優(yōu)化原則连锯。
- Q&A运怖。
項(xiàng)目審查
項(xiàng)目審查是Unity技術(shù)支持工程師的核心工作摇展。我們前往客戶的辦公室咏连,通扯扑螅花上整整兩天時(shí)間來熟悉他們的項(xiàng)目,詢問他們的需求和設(shè)計(jì)決策埠偿,然后使用各種的性能分析工具冠蒋,來檢測性能瓶頸。
在結(jié)構(gòu)完善的項(xiàng)目中斩郎,例如:利用模塊化場景和AssetBundles等功能的項(xiàng)目缩宜,構(gòu)建時(shí)間一般較短锻煌,我們可以在現(xiàn)場優(yōu)化宋梧,然后重新分析場景,確定是否有新的問題跺讯。
此時(shí)優(yōu)化構(gòu)建時(shí)間的重要性就體現(xiàn)出來了:好的優(yōu)化可以實(shí)現(xiàn)更頻繁的迭代刀脏。對于開發(fā)硬件和目標(biāo)硬件之間區(qū)別較大的項(xiàng)目來說耀态,例如:面向移動(dòng)設(shè)備或游戲主機(jī)的項(xiàng)目首装,構(gòu)建時(shí)間的優(yōu)化就更重要了仙逻。
由于我們的客戶非常多樣,他們的項(xiàng)目類型涵蓋了各種目標(biāo)平臺(tái)和需求缺亮,因此項(xiàng)目審查的工作內(nèi)容也不盡相同萌踱。如果在現(xiàn)場工作時(shí)并鸵,我們遇到了沒能解決的問題能真,我們會(huì)盡量收集信息,在回到Unity辦公室后蝙泼,繼續(xù)進(jìn)行進(jìn)一步的調(diào)查汤踏,必要的話還會(huì)咨詢R&D的同事。
最終結(jié)果會(huì)取決于客戶需求哗脖,但通常情況下才避,我們會(huì)提供一份總結(jié)調(diào)查結(jié)果和建議方案的書面報(bào)告棘劣。在決定關(guān)注的重點(diǎn)時(shí)茬暇,我們的目標(biāo)是始終為客戶提供最有價(jià)值的信息。
雖然我們有Unity的源代碼,但在進(jìn)行項(xiàng)目審查時(shí)珊皿,我們會(huì)盡量將自己放在客戶的位置上考慮。也就是說驶兜,我們會(huì)使用網(wǎng)上可找到的性能分析工具和最佳實(shí)踐抄淑,來優(yōu)化客戶的項(xiàng)目。
如果我們需要深入研究來找到性能問題的根本原因郑原,那么在問題解決后犯犁,我們會(huì)盡可能對相關(guān)文檔進(jìn)行更新,從而將這些新知識(shí)提供給所有用戶簇捍,給用戶帶來更大的便利吼句。
CPU綁定與GPU綁定
正如演講里說的惕艳,在開始優(yōu)化項(xiàng)目前,我們需要找出實(shí)際的瓶頸究竟在哪里谁鳍。一個(gè)可行的方法是使用Unity Profiler性能分析器倘潜,來查看CPU的使用狀況。
如下圖所示养泡,如果大部分的幀時(shí)間都用在“Rendering”(渲染)部分,首先我們要確定項(xiàng)目是CPU綁定還是GPU綁定输硝。
渲染過程會(huì)同時(shí)在CPU和GPU上執(zhí)行程梦,本文不會(huì)詳細(xì)介紹渲染過程点把,簡單來說,對于共用同一個(gè)材質(zhì)的對象分組屿附,場景的渲染過程主要包含以下步驟:
CPU將一系列指令傳遞給GPU郎逃,從而設(shè)置好內(nèi)部狀態(tài),例如:著色器挺份、綁定紋理、頂點(diǎn)格式等部分。該步驟又稱為“set pass(設(shè)置通道)”調(diào)用优训。
CPU將幾何體的批處理傳遞給GPU朵你,讓GPU根據(jù)上一步設(shè)置的狀態(tài)來渲染幾何體。這一步稱為“Draw Call(繪制調(diào)用)”揣非,耗費(fèi)性能較大抡医。
如果有更多相同材質(zhì)類型的幾何體需要渲染,那么就重復(fù)第二步操作早敬。
以上操作的相應(yīng)算法有許多的細(xì)節(jié)和注意事項(xiàng)忌傻,但關(guān)鍵點(diǎn)在于渲染過程會(huì)同時(shí)在CPU和GPU上執(zhí)行。如下圖所示搞监,Xcode這類工具可以給我們提供CPU和GPU資源所花的具體時(shí)間水孩。
我們也可以在Unity性能分析器找到這類信息,不過由于這些信息取決于顯卡和驅(qū)動(dòng)提供的支持琐驴,因此GPU數(shù)據(jù)不總是會(huì)出現(xiàn)俘种。
如果我們無法使用性能分析工具獲取CPU和GPU的使用時(shí)間,則可以在性能分析器中觀察任一幀的情況绝淡。
如果存在對Gfx.WaitForPresent的調(diào)用安疗,并且顯示“調(diào)用花費(fèi)了不少時(shí)間”,這意味著CPU正在等待GPU完成所有的渲染指令够委,說明該項(xiàng)目屬于GPU綁定。
了解WaitForTargetFPS和Gfx.PresentFrame等標(biāo)記的含義:
https://docs.unity3d.com/Manual/ProfilerCPU.html
對GPU工作量影響較大的因素有以下幾種:
Fill rate(填充率):在特定幀上怖现,應(yīng)用程序多次在過多的像素上進(jìn)行著色茁帽,該過程稱為“Overdraw”(過度繪制)。
內(nèi)存帶寬:應(yīng)用程序可能會(huì)給GPU發(fā)送大量紋理數(shù)據(jù)屈嗤。如果想減少內(nèi)存占用潘拨,我們可以使用紋理圖集來減少紋理數(shù)量,減少紋理大小饶号,或在條件允許時(shí)將紋理壓縮為特定格式铁追。
頂點(diǎn)處理:應(yīng)用程序向GPU發(fā)送過多的幾何體。在Unite的演講中茫船,我們把這種情況作為案例進(jìn)行了講解琅束。
此外,如果項(xiàng)目屬于CPU綁定算谈,那么會(huì)有許多東西會(huì)增加CPU時(shí)間涩禀,例如:物理、游戲代碼等然眼,我們同樣應(yīng)該查看性能分析器艾船。
如果性能分析器告訴我們在渲染上花了很多時(shí)間,這可能表示CPU忙于給GPU傳遞過多的指令。這種情況可以通過減少狀態(tài)變更的次數(shù)(或“SetPass”調(diào)用)和批處理次數(shù)屿岂,來實(shí)現(xiàn)優(yōu)化的目的践宴。
關(guān)于此問題的深入討論,請閱讀《Fixing Performance Problems(修補(bǔ)性能問題)》教程:
https://learn.unity.com/tutorial/fixing-performance-problems#5c7f8528edbc2a002053b596
案例學(xué)習(xí):加載數(shù)據(jù)時(shí)出現(xiàn)CPU峰值情況
在客戶的項(xiàng)目中爷怀,一個(gè)常見問題是:在應(yīng)用程序啟動(dòng)時(shí)阻肩,或進(jìn)入新關(guān)卡的時(shí)候,可能會(huì)出現(xiàn)的性能卡頓問題霉撵。
在Unity性能分析器中磺浙,這類卡頓問題會(huì)顯示為峰值情況。
卡頓的原因主要是:性能開銷較大的處理過程和大量的內(nèi)存分配徒坡。在本案例中撕氧,CPU峰值情況造成了近10秒的卡頓,托管分配的內(nèi)存量達(dá)到3.8 GB喇完,如下圖所示伦泥。
這種峰值情況應(yīng)該是要避免的,主要有兩個(gè)原因:
過長的運(yùn)算時(shí)間會(huì)中斷應(yīng)用的運(yùn)行過程锦溪。掩蓋卡頓情況的一種方法是使用加載界面不脯,然而如果畫面中需要展示動(dòng)態(tài)元素,那么該方法不會(huì)起到作用刻诊,因?yàn)閯?dòng)畫也會(huì)在加載時(shí)發(fā)生卡頓防楷。
大量的內(nèi)存分配會(huì)造成托管堆的大小永久增大。Unity的自動(dòng)內(nèi)存管理系統(tǒng)的工作方式是:沒有引用的內(nèi)存會(huì)在后續(xù)分配過程中重用则涯,但是托管堆的大小不會(huì)減小复局,而是只會(huì)增加。這種情況稱為“非壓縮式垃圾回收(Non-compacting garbage collection)”粟判。
請參考Unity官方文檔和Unity Learn的文章中了解更多信息亿昏。
Unity官方文檔:
https://docs.unity3d.com/Manual/UnderstandingAutomaticMemoryManagement.html
Unity Learn文章:
https://learn.unity.com/tutorial/fixing-performance-problems#5c7f8528edbc2a002053b595
峰值情況往往由多種因素造成。根據(jù)我們的實(shí)地經(jīng)驗(yàn)档礁,其中一個(gè)原因是應(yīng)用程序使用了未優(yōu)化的格式來存儲(chǔ)數(shù)據(jù)角钩。例如:JSON格式和XML格式,而解析器需要分配大量內(nèi)存來繼續(xù)處理應(yīng)用內(nèi)容呻澜。除了這種分配外递礼,對這類數(shù)據(jù)執(zhí)行的大量計(jì)算和相應(yīng)的內(nèi)存分配都是罪魁禍?zhǔn)住?/p>
為了減小這些問題的影響,我們通常建議客戶實(shí)現(xiàn)“預(yù)算時(shí)間管理器”系統(tǒng)羹幸,它會(huì)在每幀限制中實(shí)例化和初始化對象宰衙,并添加對二進(jìn)制格式的支持《糜“預(yù)算時(shí)間管理器”會(huì)將運(yùn)算分散到多個(gè)幀上供炼,而對二進(jìn)制數(shù)據(jù)的支持則會(huì)減少內(nèi)存分配的大小一屋。
“預(yù)算時(shí)間管理器”與使用單個(gè)方法加載所有數(shù)據(jù)之間的區(qū)別,類似普通垃圾回收器和增量式垃圾回收功能的區(qū)別:前者會(huì)在某一幀上卡頓袋哼,直到整個(gè)列表的托管對象處理完畢冀墨,而后者會(huì)將處理過程分散到多個(gè)幀上執(zhí)行。
由于它們存在的本質(zhì)特點(diǎn)涛贯,二進(jìn)制數(shù)據(jù)通常難以在開發(fā)中使用诽嘉。因此我們建議客戶不要完全移除對文本格式的支持,而是支持和使用文本及二進(jìn)制兩種格式弟翘,具體取決于客戶執(zhí)行的是應(yīng)用程序的開發(fā)版本還是發(fā)布版本虫腋。
垃圾回收過程
在演講的《GC spikes in a fast-paced game”(快節(jié)奏游戲的GC峰值情況》案例中,我們建議客戶啟用Incremental Garbage Collector增量式垃圾回收功能稀余,盡可能減少幀時(shí)間悦冀,給算法足夠的空間在每幀的結(jié)束時(shí)完成執(zhí)行過程。
但在演講中睛琳,沒有足夠強(qiáng)調(diào)的一點(diǎn)是:對于最小化托管內(nèi)存分配的數(shù)量和大小盒蟆,增量式垃圾回收功能不是讓該過程不夠嚴(yán)謹(jǐn)?shù)慕杩凇?/p>
和常規(guī)垃圾回收器相比,增量式垃圾回收優(yōu)點(diǎn)是可以將工作量分布到多個(gè)幀上完成师骗,從而避免在單個(gè)幀上等待整個(gè)托管對象池處理完畢历等,這對維持穩(wěn)定的幀率非常重要。
我們可以把GarbageCollector.GCMode靜態(tài)字段的值更改為GarbageCollector.Mode.Disabled辟癌,從而禁用垃圾回收器寒屯。
GarbageCollector.GCMode = GarbageCollector.Mode.Disabled;
如果不想產(chǎn)生垃圾回收算法相關(guān)的處理開銷,我們可以使用這個(gè)方法黍少。但需要注意寡夹,使用這種方法時(shí),我們需要確保在垃圾回收器禁用時(shí)仍侥,不會(huì)發(fā)生任何內(nèi)存分配操作。
因?yàn)榫腿缪葜v中所說鸳君,在內(nèi)存使用量超過特定閾值時(shí)污淋,操作系統(tǒng)會(huì)主動(dòng)關(guān)閉應(yīng)用程序忍弛。在Android和iOS等移動(dòng)平臺(tái)上,這種情況更加明顯。
案例學(xué)習(xí):使用權(quán)威服務(wù)器的FPS游戲
權(quán)威服務(wù)器架構(gòu)(Authoritative Server Architecture)危纫,這種架構(gòu)的網(wǎng)絡(luò)游戲是游戲客戶端將輸入(按鍵,命令)發(fā)送到服務(wù)器凌唬,服務(wù)器運(yùn)行游戲缠劝,然后將結(jié)果發(fā)送回客戶端。之所以稱之為“權(quán)威服務(wù)器”平挑,是因?yàn)殛P(guān)于游戲世界中發(fā)生的一切游添,唯一有權(quán)限處理的只有服務(wù)器系草。
幾個(gè)月前我們對一款第一人稱多人射擊游戲進(jìn)行項(xiàng)目審查,該游戲擁有一個(gè)權(quán)威服務(wù)器架構(gòu)唆涝,而服務(wù)器通過無頭模式(Headless mode)運(yùn)行找都。
我們使用了Unity Memory Profiler內(nèi)存分析器來獲取內(nèi)存信息,發(fā)現(xiàn)無頭服務(wù)器上有許多不必要的網(wǎng)格廊酣、光照探針能耻、音頻剪輯、網(wǎng)格渲染器和各種其它類型的對象亡驰,累計(jì)分配了上百M(fèi)B的內(nèi)存晓猛。
雖然這些額外的內(nèi)存占用沒有影響服務(wù)器運(yùn)行單個(gè)多人游戲會(huì)話,但是卻明顯影響到了游戲的可擴(kuò)展性凡辱。具體來說戒职,在特定服務(wù)器上增加活動(dòng)實(shí)例數(shù)量會(huì)需要更多的內(nèi)存。
在這種情況下煞茫,我們建議客戶把每個(gè)游戲關(guān)卡場景分為兩個(gè)部分帕涌,分別保存到不同的AssetBundle中。第一個(gè)部分是“邏輯場景”续徽,其中包含了無頭服務(wù)器需要的所有信息蚓曼,第二個(gè)部分是“可視場景”,其中包含了只有客戶端使用的所有信息钦扭。
這種劃分操作可能會(huì)造成工作流程上的問題纫版。更具體來說,美術(shù)師和關(guān)卡設(shè)計(jì)師將不能在同一個(gè)場景中協(xié)作客情。因此我們沒有讓客戶變更內(nèi)容創(chuàng)作者的工作流程其弊,而是建議保持原有創(chuàng)作流程,在構(gòu)建流程中額外加入劃分為“邏輯場景”和“可視場景”的流程膀斋。
深度分析與分析器標(biāo)記
我們的目標(biāo)是在應(yīng)用程序的核心運(yùn)行循環(huán)中把內(nèi)存分配降到接近0/幀梭伐。減少內(nèi)存分配會(huì)減少垃圾回收算法產(chǎn)生的開銷。
Unity性能分析器是最能勝任的工具仰担,但在調(diào)用堆棧報(bào)告中糊识,默認(rèn)深度層次只能是引擎原生代碼在轉(zhuǎn)入應(yīng)用編程代碼中首次調(diào)用堆棧深度一致,例如:MonoBehaviour.Start()摔蓝、MonoBehaviour. Update()及類似的方法赂苗。
在實(shí)際使用時(shí),如果腳本調(diào)用了其它腳本的方法贮尉,我們無法輕易找到發(fā)生托管內(nèi)存分配的具體位置拌滋。
解決問題的一種方法是:給腳本加上Profiler Markers分析器標(biāo)記。這樣我們可以在性能分析時(shí)記錄額外的信息猜谚,幫助我們縮小內(nèi)存分配的來源范圍败砂。
另一種方法是:啟用Deep Profiling深度分析功能赌渣。
深度分析功能的具體操作請閱讀Unity Learn的文章:
https://learn.unity.com/tutorial/profiling-applications-made-with-unity#5c7f8528edbc2a002053b5b6
請注意,深度分析功能會(huì)增加更多性能開銷吠卷,大幅減慢應(yīng)用運(yùn)行速度锡垄,因此報(bào)告記錄的時(shí)間并不準(zhǔn)確。
我們建議首先在禁用深度分析過程時(shí)祭隔,運(yùn)行性能分析流程货岭,記錄造成額外托管內(nèi)存分配的情況,如果調(diào)用棧報(bào)告不能提供足夠的信息來找出分配來源疾渴,那么我們要使用深度分析功能進(jìn)行第二次分析千贯,從而找到這些分配的來源。
在Unity 2019.3之前搞坝,深度分析功能只可以在使用Mono腳本后端時(shí)使用搔谴。Unity 2019.3的Beta版已經(jīng)不存在這個(gè)限制,深度分析功能同時(shí)支持Mono后端和IL2CPP后端桩撮。
下面是Unity 2019.3 Beta的部分發(fā)行說明:
- Profiler:添加了Deep Profiler深度分析器對Mono后端和IL2CPP后端的支持敦第。
- Profiler:添加了Deep Profiling深度分析器對運(yùn)行版本構(gòu)建選項(xiàng)的支持。當(dāng)使用Deep Profiling深度分析功能構(gòu)建運(yùn)行版本時(shí)店量,可以動(dòng)態(tài)地啟用或禁用C#代碼插裝功能(C# Code Instrumentation)芜果。
- Profiler:對運(yùn)行版本添加了托管內(nèi)存分配調(diào)用棧的支持。當(dāng)啟用調(diào)用椚谑Γ回收時(shí)右钾,GC.Alloc的樣本將包含C#代碼調(diào)用棧。
由于深度分析功能現(xiàn)在支持IL2CPP后端旱爆,開發(fā)者可以在iOS這類只支持IL2CPP的平臺(tái)上執(zhí)行深度分析數(shù)據(jù)收集舀射。此外,新增的對托管內(nèi)存分配調(diào)用棧的支持怀伦,可以幫助開發(fā)者找到運(yùn)行版本內(nèi)存分配來源脆烟,不必進(jìn)行深度分析。
結(jié)語
性能優(yōu)化是一個(gè)很大的話題房待,需要開發(fā)者掌握各式各樣的技能邢羔,例如:了解底層硬件的操作原理和局限。
了解Unity提供的各種類和組件吴攒,算法和數(shù)據(jù)結(jié)構(gòu)张抄,并且要知道如何使用性能分析工具砂蔽,開發(fā)者也需要發(fā)揮一定的創(chuàng)造力洼怔,來找到能夠滿足設(shè)計(jì)需求的高效解決方案。