特殊優(yōu)化

原文鏈接:https://docs.unity3d.com/Manual/BestPracticeUnderstandingPerformanceInUnity8.html

? ? ? ? 前面的章節(jié)敘述了對所有項目適用的優(yōu)化手段,本章的優(yōu)化手段不應(yīng)該在收集性能分析數(shù)據(jù)之前應(yīng)用。這可能是因為這些優(yōu)化手段實現(xiàn)起來是勞動密集型的,可能是因為為了支持性能而損失代碼的整潔性和可維護性,或者可能是因為這些問題只在某種特定的程度上才會出現(xiàn)。


多維數(shù)組和交錯數(shù)組

? ? ? ? 像這個StackOverflow文章(https://stackoverflow.com/questions/597720/what-are-the-differences-between-a-multidimensional-array-and-an-array-of-arrays)中所敘述的,通常迭代交錯數(shù)組比迭代多維數(shù)組更有效率,因為多維數(shù)組需要函數(shù)調(diào)用。

? ? ? ? 請注意:

? ? ? ? ·他們是數(shù)組的數(shù)組,使用type[x][y]聲明而不是type[x,y]

? ? ? ? ·可以使用ILSpy或者類似工具監(jiān)測訪問多維數(shù)組的IL生成來發(fā)現(xiàn)。

? ? ? ? 在Unity5.3中進行性能測試,通過對一個三維的100x100x100的數(shù)組進行完全順序迭代產(chǎn)生下面的時間數(shù)據(jù),它們是運行10次測試取平均值的結(jié)果:

? ? ? ? 數(shù)組類型? 花費時間 (100次迭代)

? ? ? ? 一維數(shù)組? 660 ms

? ? ? ? 交錯數(shù)組? 730 ms

? ? ? ? 多維數(shù)組? 3470 ms

? ? ? ? 額外函數(shù)調(diào)用的花費可以從訪問多維數(shù)組和一維數(shù)組花費的不同中看出,對于迭代非緊湊內(nèi)存數(shù)據(jù)結(jié)構(gòu)的花費可以從訪問交錯數(shù)組和一維數(shù)組的比較中看到不同。

? ? ? ? 正如上面的演示,額外函數(shù)調(diào)用的消耗大大超過了使用非緊湊型內(nèi)存數(shù)據(jù)結(jié)構(gòu)消耗造成的影響。

? ? ? ? 對于高度性能敏感的操作來說,強烈建議使用一維數(shù)組。對于其他有多維需求的數(shù)組來說,應(yīng)該使用交錯數(shù)組。多維數(shù)組不應(yīng)該使用。


粒子系統(tǒng)池

? ? ? ? 在緩存粒子系統(tǒng)時,請注意它們至少會消耗3500字節(jié)的內(nèi)存。內(nèi)存消耗會隨著粒子系統(tǒng)上激活的模塊數(shù)量而增長。這個內(nèi)存當粒子系統(tǒng)被禁用時不會被釋放,只有它們被銷毀時才會被釋放。

? ? ? ? 在Unity5.3中,大多數(shù)的粒子系統(tǒng)設(shè)置可以在運行時執(zhí)行。對于那些必須緩存大量不同粒子效果的項目,把粒子系統(tǒng)的配置參數(shù)提取到一個數(shù)據(jù)承載類或是一個結(jié)構(gòu)體上可能會更有效率。

? ? ? ? 當一個粒子特效被需要時,一個通用的粒子效果池可以提供必備的粒子效果對象。配置數(shù)據(jù)可以被應(yīng)用到對象中來達到想要的圖形效果。

? ? ? ? 這事實上比緩存所有給定場景的粒子系統(tǒng)的變體和配置更有內(nèi)存效率,但這需要大量的編程工作才能達到。


更新管理器

? ? ? ? 內(nèi)部,Unity追蹤注冊其回調(diào)的對象列表,比如Update, FixedUpdate和LateUpdate。它們是作為侵入式鏈表進行維護,這樣來確保列表的更新在固定的時間內(nèi)。當啟用和禁用時,MonoBehaviour被從這些列表中添加或移除。

? ? ? ? 雖然給所需的MonoBehaviour簡單的添加適當?shù)幕卣{(diào)函數(shù)是方便的,但是當回調(diào)函數(shù)數(shù)量增加時效率會變的越來越低。底層代碼調(diào)用托管代碼存在一個小但是顯著的消耗。其結(jié)果不單是在執(zhí)行大量的每幀函數(shù)時降低幀時間,也會在初始化包含大量MonoBehaviour的預(yù)制體時降低初始化時間(請注意:初始化的消耗取決于這個預(yù)制體每個組件中執(zhí)行Awake和OnEnable回調(diào)的性能消耗)。

? ? ? ? 當每幀進行的回調(diào)的MonoBehaviour的數(shù)量增長到成百上千,移除這些回調(diào)轉(zhuǎn)而將這些MonoBehaviour(或是獨立的C#對象)關(guān)聯(lián)到一個全局單例管理器上。這個全局單例管理器可以分發(fā)Update, LateUpdate和其他回調(diào)到感興趣的對象上。這樣所另外一個額外的好處是允許代碼在沒有操作的情況下巧妙的取消回調(diào)函數(shù)的注冊,從而減少每幀必須調(diào)用函數(shù)的絕對數(shù)量。

? ? ? ? 通常通過很少執(zhí)行的回調(diào)來實現(xiàn)最大的性能節(jié)省。考慮下面的偽代碼:

void Update() {

? ? if(!someVeryRareCondition) { return; }

// … some operation …

}

? ? ? ? 如果像上面那樣有大量的擁有Update回調(diào)的MonoBehaviour,那么大量的時間消耗將花費在Update回調(diào)上,它們在MonoBehaviour執(zhí)行時在底層和托管域切換,然后迅速退出。如果這些類轉(zhuǎn)而注冊到一個全局Update管理器上,只有當someVeryRareCondition為true時才生效,并且隨后將其取消注冊,那么時間將會從切換代碼域和評估很少的條件中節(jié)省。


在一個update管理器中使用委托

? ? ? ? 簡單的使用C#委托來實現(xiàn)這些回調(diào)是簡單的。然而C#委托為低頻率的注冊和取消注冊、少量的回調(diào)數(shù)量進行了優(yōu)化。每當一個回調(diào)被添加或者移除時,C#委托都會對回調(diào)列表執(zhí)行一個完全的深度拷貝。龐大的回調(diào)列表,或是大量的回調(diào)添加移除操作在同一幀會在內(nèi)部的Delegate.Combine函數(shù)中造成一個性能尖峰。

? ? ? ? 對于那些添加和移除高頻發(fā)生,考慮使用一個插入移除快的數(shù)據(jù)結(jié)構(gòu)來代替委托。


加載線程控制

? ? ? ? Unity允許開發(fā)者控制被用于加載數(shù)據(jù)的后臺線程的優(yōu)先級。這對于在后臺流shift傳輸AssetBundle到磁盤上尤其重要。

? ? ? ? 主線程和圖形線程的優(yōu)先級都是ThreadPriority.Normal——任何高優(yōu)先級的線程都會搶占主線程或是圖形線程,并造成幀率卡頓,反之低優(yōu)先級的線程就不會這樣。如果一個線程與主線程有相同的優(yōu)先級,CPU會嘗試給這些線程相同的時間,如果有多個后臺線程在進行比如說AssetBundle解壓這樣的繁重操作時,通常會造成幀率的卡頓。

? ? ? ? 目前,這個優(yōu)先級可以在三個地方控制。

? ? ? ? 首先,對于Asset加載調(diào)用的默認優(yōu)先級,比如說Resources.LoadAsync和AssetBundle.LoadAssetAsync,來自Application.backgroundLoadingPriority設(shè)置。正如文當中所說,這個調(diào)用也限制了主線程花費在集成Asset上的時間(請注意,大多數(shù)Unity的Asset類型必須被集成到主線程上。在集成期間,這些Asset的初始化工作會被完成,以及某些線程安全操作將會被執(zhí)行。這包括腳本回調(diào)執(zhí)行,比如說Awake回調(diào)。請看“Resource Management” 指導(dǎo)獲取進一步的細節(jié)。),為了限制Asset加載對幀時間的影響。

? ? ? ? 其次,每個異步的Asset加載操作,比如UnityWebRequest請求,返回一個 AsyncOperation對象到監(jiān)視器并且管理這個操作。這個AsyncOperation對象暴露一個priority屬性可以用于調(diào)整一個獨立操作的優(yōu)先級。

? ? ? ? 最后,WWW對象,比如那些從WWW.LoadFromCacheOrDownload返回的調(diào)用,會暴露一個threadPriority屬性。重要提示,WWW對象不會自動使用Application.backgroundLoadingPriority設(shè)置作為其默認值-WWW對象的默認值總是ThreadPriority.Normal。

? ? ? ? 值得注意的是,底層系統(tǒng)用于解壓和加載的方式不同于這些api。Resources.LoadAsync和AssetBundle.LoadAssetAsync是通過Unity內(nèi)部的PreloadManager系統(tǒng)來執(zhí)行的,其會管理它自己的加載線程和執(zhí)行它自己的幀率限制。UnityWebRequest有其專用的線程池,每當一個新的請求被創(chuàng)建時,WWW會生成一個全新的線程。

? ? ? ? 其他的加載機制有內(nèi)置的隊列系統(tǒng),而WWW并沒有。在大量被壓縮的AssetBundle上調(diào)用WWW.LoadFromCacheOrDownload會生成相等數(shù)量的線程,它們將與主線程爭奪CPU時間,很容易造成幀率卡頓。

? ? ? ? 所以,當使用WWW加載和解壓AssetBundle時,最好是對于創(chuàng)建的每一個WWW對象都設(shè)置合適的threadPriority值。

? ? ? ? 其他的加載機制有內(nèi)置的隊列系統(tǒng),而WWW并沒有。在大量被壓縮的AssetBundle上調(diào)用WWW.LoadFromCacheOrDownload會生成相等數(shù)量的線程,它們將與主線程爭奪CPU時間,很容易造成幀率卡頓。

? ? ? ? 所以,當使用WWW加載和解壓AssetBundle時,最好是對于創(chuàng)建的每一個WWW對象都設(shè)置合適的threadPriority值。


聚集的對象移動和剔除組

? ? ? ? 正如在變換處理章節(jié)中提到的,由于變更消息的傳播,移動大量的變換層級會有一個相對高的cpu消耗。然而,在真實的開發(fā)環(huán)境中,通常無法將層級折疊成一個適度數(shù)量的游戲物體。

? ? ? ? 與此同時,良好的開發(fā)實踐是運行足夠的行為來維持游戲世界的可信度,同時消除用戶不會注意到的行為。比如,在一個有大量角色的場景中,更優(yōu)化的做法是只對屏幕上的角色進行蒙皮網(wǎng)格和動畫驅(qū)動的移動變換。完全沒有理由浪費CPU的時間在對屏幕外的角色進行計算完全的視覺元素模擬。

? ? ? ? 這些問題可以通過在Unity 5.1中引入的新API:CullingGroups巧妙的解決。

? ? ? ? 不是直接操縱場景中的一組大量的游戲物體,而是改變系統(tǒng)來操縱一個CullingGroup組中一組BoundingSphere的Vector3參數(shù)。每個BoundingSphere充當著一個游戲邏輯實體的世界空間坐標的命令庫,并且當實體移動到CullingGroup的主攝像機附近或其中時接收回調(diào)函數(shù)。這些回調(diào)函數(shù)用于激活或是取消激活代碼或是組件(比如Animator),管理那些只應(yīng)該在實體可見時運行的行為。


減少函數(shù)調(diào)用消耗

? ? ? ? C#的字符串庫提供了一個非常好的研究案例,針對增加額外的簡單代碼庫對的函數(shù)調(diào)用的花費。在內(nèi)置的string API的String.StartsWith和String.EndsWith部分中,提到了用手寫代碼替代內(nèi)置的函數(shù),速度提升了10-100倍,即使當不必要的語言環(huán)境強制被抑制。

? ? ? ? 性能不同的關(guān)鍵問題是在緊湊的內(nèi)部循環(huán)中簡單的增加了額外函數(shù)的調(diào)用。每個執(zhí)行的函數(shù)都必須定位到這個函數(shù)在內(nèi)存中的地址,并將另一幀推到堆棧上。這些操作都不是免費的,帶是在大多數(shù)代碼中它都足夠小到可以被忽略。

? ? ? ? 然而,當在緊湊的循環(huán)中運行小的函數(shù)時,引入額外函數(shù)調(diào)用造成的消耗增加會變得巨大-并且甚至會非常明顯。

? ? ? ? 考慮下面兩個簡單的例子:

例1:

int Accum { get; set; }

Accum = 0;

for(int i = 0; i < myList.Count; i++) {

? ? Accum += myList[i];

}

例2:

int accum = 0;

int len = myList.Count;

for(int i = 0; i < len; i++) {

? ? accum += myList[i];

}

? ? ? ? 這些函數(shù)都計算了一個C#類List<int>中所有整數(shù)的和,例1更“現(xiàn)代化C#”一些,因為其使用了一個自動生成的屬性來保存其數(shù)據(jù)的值。

? ? ? ? 雖然表面上講這兩段代碼看起來是相等的,但是當代碼使用函數(shù)調(diào)用來分析時其區(qū)別是顯著的。

例1:

int Accum { get; set; }

Accum = 0;

for(int i = 0;

? ? ? i < myList.Count;? ? //調(diào)用List::getCount

? ? ? i++) {

? ? Accum? ? ? //調(diào)用set_Accum

+=? ? ? //調(diào)用get_Accum

myList[i];? //調(diào)用List::get_Value

}

? ? ? ? 所以在每次循環(huán)執(zhí)行時會調(diào)用四個函數(shù):

? ? ? ? ·myList.Count在Count屬性上執(zhí)行g(shù)et函數(shù)?

? ? ? ? ·在Accum屬性上肯定會執(zhí)行g(shù)et和set函數(shù)

? ? ? ? ·執(zhí)行g(shù)et函數(shù)來獲取Accum當前的值以便其可以傳遞到加法操作中

? ? ? ? ·執(zhí)行set函數(shù)來將加法操作的結(jié)果設(shè)置到Accum中

? ? ? ? ·[]操作符執(zhí)行l(wèi)ist的get_Value函數(shù)來獲取list中指定索引的元素值

Example 2:

int accum = 0;

int len = myList.Count;

for(int i = 0;

? ? i < len;

? ? i++) {

? ? accum += myList[i]; //調(diào)用List::get_Value

}

? ? ? ? 在這第二個例子中,調(diào)用get_Value保留,但是其他所有的函數(shù)調(diào)用都被消除了或是不再每次循環(huán)迭代執(zhí)行一次。

? ? ? ? ·由于accum現(xiàn)在使用了原始值來代替屬性,在設(shè)置或是獲取它的值時不再需要進行函數(shù)調(diào)用

? ? ? ? ·由于當循環(huán)進行時myList.Count采取了不再變化,其訪問已經(jīng)被移到了循環(huán)條件狀態(tài)之外,所以其不再會在每次循環(huán)迭代的開始時執(zhí)行。

? ? ? ? 這兩個版本代碼的時間花費顯示了從這個特定代碼片段中移除75%函數(shù)調(diào)用的益處。當我們在現(xiàn)代桌面平臺機器上運行100000次時:

? ? ? ? ·例1需要324毫秒來執(zhí)行

? ? ? ? ·例2需要128毫秒來執(zhí)行

? ? ? ? 這里主要的問題是Unity幾乎不執(zhí)行函數(shù)的內(nèi)聯(lián),如果有的話也非常少。即使在IL2CPP下,許多函數(shù)當前也沒有正確的內(nèi)聯(lián)。尤其是對于屬性。進一步說,虛函數(shù)和接口函數(shù)完全不能內(nèi)聯(lián)。

? ? ? ? 因此,一個在C#源碼中的函數(shù)調(diào)用非常可能最終在二進制應(yīng)用程序中產(chǎn)生一個函數(shù)調(diào)用。


瑣碎的屬性

? ? ? ? Unity在其數(shù)據(jù)類型中提供了許多“簡單的”常量使開發(fā)者能夠方便的開發(fā)。然而,鑒于上面的情況,要非常注意,這些常量值通常是通過返回常量值的屬性來實現(xiàn)的。

? ? ? ? Vector3.zero的屬性體如下:

get { return new Vector3(0,0,0); }

? ? ? ? Quaternion.identity也非常相似:

get { return new Quaternion(0,0,0,1); }

? ? ? ? ?雖然與圍繞它們的實際代碼相比,訪問這些屬性的消耗非常小。但是當它們每幀執(zhí)行上千遍或是更多時就會造成小的影響。

? ? ? ? 對于簡單的原始類型,使用const值來代替.Const值會在編譯時被內(nèi)聯(lián)——到const變量的引用會被其值替換。

? ? ? ? 請注意:由于每個到const變量的引用都替換成了它的值,那么就不建議在const中聲明長的字符串和其他大的數(shù)據(jù)類型。沒必要因為在最終的指令代碼中都是重復(fù)的數(shù)據(jù)而造成最終的二進制文件膨脹。

? ? ? ? 當const不合適時,使用一個static readonly來代替。在一些項目中,即使Unity內(nèi)置的瑣碎屬性也已經(jīng)被替換成了 static readonly變量,這樣可以在性能上有小的提升。


瑣碎的函數(shù)

? ? ? ? 瑣碎的函數(shù)是一個騙局。它在聲明一個功能并且在另一個地方重用時非常有效。然而在緊湊的循環(huán)中,也許有必要離開某些良好的編碼實踐并且轉(zhuǎn)而使用“手動的內(nèi)聯(lián)”某些代碼。

? ? ? ? 一些函數(shù)可以被完全消除??紤]Quaternion.Set,Transform.Translate或是Vector3.Scale。他們執(zhí)行非?,嵥榈牟僮鞑⑶铱梢员缓唵蔚馁x值語句來代替。

? ? ? ? 對于更為復(fù)雜的函數(shù),請權(quán)衡分析手動內(nèi)聯(lián)性能的證據(jù)與維持更高性能的代碼的長期成本。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容