C#代碼優化:拯救你的CPU耗時

之前,我們已經對本地資源檢測中和資源/Prefab的內容做了總結,後續UWA也會和大家一起努力,進一步豐富這些檢測內容。今天我們要聚焦的是本地資源檢測中的C#代碼相關的檢測項。

要保證遊戲在流暢的幀率下運行,就要保證CPU和GPU能夠及時地完成它們在一幀當中的“任務”。而本文我們講解的這些C#代碼的性能,就會影響到每幀CPU自身的耗時。當遊戲出現幀率降低、間歇性卡頓甚至卡死等現象時,我們就需要考慮這是否是由開發者自己所編寫的腳本性能較差所造成的。

性能的優化問題,實際上是個“時空問題”:我們要儘可能地節省運算所需的時間,節約內存上佔用的空間。對C#代碼的優化亦是如此,一方面是對CPU耗時的優化,一方面是對內存分配的優化。而“空間問題”與“時間問題”又常常會相互轉化——優化內存的目的之一,是減少GC,這又可以歸結爲減少CPU耗時。本文要講解的一系列規則,就是主要針對CPU耗時的規則。

1、類中存在OnGUI方法

規則裏涉及到的OnGUI,它是Unity的IMGUI系統繪製UI所調用的方法。該方法如果寫在繼承了Monobehavior的腳本上,那麼Unity會在每一幀自動對其進行調用。

使用IMGUI進行UI繪製,想要更改任何內容,整個圖形用戶界面都要重新繪製,OnGUI會在一幀當中調用多次,這會導致CPU耗時增加。此外,如果OnGUI函數使用不當,容易造成堆內存的持續分配。因此,在遊戲項目中,一般不使用IMGUI進行UI開發(常用的有UGUI、NGUI等)。IMGUI一般用於編輯器擴展開發、遊戲調試面板繪製等。


2、類中存在空的Update、LateUpdate和FixedUpdate方法

我們在上面的規則中簡單說到了“Monobehavior”,Unity中的腳本其實默認都是繼承自這個Monobehavior。Update、LateUpdate和FixedUpdate屬於Monobehaviour類的“Messages”,雖然不由Monobehavior類繼承而來,但是在Monobehaviour類的腳本中會生效——如果腳本中寫上了這些方法,相應的腳本放到場景中,並且enable爲true,那麼遊戲運行過程中每幀都會對其進行調用。

即使這些方法爲空,在運行時,它們依然會因爲被調用而造成CPU時間的開銷,其原因主要有兩點:

  1. 這些方法是Native層對託管層的調用,C++與C#之間的通信本身存在一定的開銷。
  2. 當調用這些方法時,Unity會進行一系列安全檢測(比如確保GameObject沒有被銷燬等)導致CPU時間的消耗。

3、該類的方法中存在Camera.main的調用

Camera.main實際上是一個實現了Get方法的屬性,每次調用它,都會尋找場景中第一個Tag爲“MainCamera”的相機並將其返回。使用舊版本的Unity對Camera.main的調用,需要遍歷所有帶Tag的GameObject、進行Tag比較、查找Camera組件等操作,耗時較高,並且引擎不會自動緩存其結果。

不過Unity 2020.2版本中已經對Camera.main進行了優化,避免了它較高的CPU耗時,使用2020.2及以上Unity版本的團隊可以忽略該規則。


4、該類的方法中存在ComputeBuffer.GetData調用

ComputeBuffer.GetData會從GPU的Buffer中讀取對應的計算結果並輸入到相應的數組中,由於整個的過程是一個同步操作,調用時會堵塞調用線程,直到GPU返回數據爲止,所以在數據量較大的時候會導致ComputeBuffer.GetData消耗很大一部分的CPU時間以及相應的堆內存空間。可以嘗試通過其他的異步操作來達到相同的取值效果。


5、該類的方法中存在對紋理SetPixels的調用

SetPixels可用於對紋理特定的mipmap層的像素進行修改,它會將一組數組的像素值賦值到貼圖的指定mipmap層,調用Apply()後會將像素傳至顯卡。需注意的是,由於Color32比Color類型所佔的空間更小,使用SetPixels32比SetPixels造成的CPU耗時也更小。所以在效果允許的情況下,我們推薦使用SetPixels32()方法來取代SetPixels()。


6、該類的方法中存在GameObject.SendMessage調用

GameObject.SendMessage用於調用相應GameObject上的腳本中的給定名稱的函數。該函數會遍歷GameObject上的所有組件以及組件腳本中的所有函數,這會導致較高的CPU開銷。所以開發者要減少不必要的SendMessage的使用。


7、該類的方法中存在GetComponentsInChildren調用/該類的方法中存在GetComponentsInParent調用

GetComponentsInChildren用於獲得當前GameObject及其子節點的所有給定類型的組件,返回的是一個包含所有符合條件的對象的數組;而GetComponentsInParent則是用於獲得當前對象及其父節點上的所有給定類型的組件。當然這裏我們在說法上忽略了對組件隱藏情況的討論。

這兩者的使用都會涉及到較大範圍內的搜索遍歷,會擠佔CPU較大的計算資源,所以開發團隊應當儘量減少相關的調用,可以嘗試緩存調用的結果,避免使其出現在Update這樣的頻繁調用的函數當中。此外,對於這兩個函數,我們建議開發團隊使用接受List類型的引用作爲參數的版本,這樣就可以避免每次調用都造成堆內存的分配。


8、該類的方法中存在FindObjectsOfType調用

如果使用FindObjectsOfType,它會對場景中的GameObject和Component進行遍歷,並將與目標Type類型相同的組件以數組的方式返回。“Find”類的操作在小型項目當中可能不會有明顯的影響,但隨着項目體量的增大,場景中物體數量的增加,該操作造成的CPU耗時也將變得不容忽視。並且該函數會造成堆內存的分配。所以我們建議儘量避免這樣的函數調用,或者通過調用一次,緩存結果的方式減少其帶來的對項目性能的影響。


9、該類的方法中存在Reflection相關函數的調用

反射(Reflection)是一項用來在代碼運行時做綁定的技術。如果代碼需要獲取的類型、調用的函數等信息是在運行時才能被明確的,那麼就需要用到反射。

但運行時綁定就意味着更高的性能開銷:項目在調用反射相關的方法時,需要獲取類型與函數等信息,並且進行參數校驗、錯誤處理、安全性檢查等。這會導致相應的CPU計算開銷較高,並且容易造成堆內存分配。因此我們建議在遊戲項目中,儘可能避免使用反射。


10、該類的方法中存在對Renderer進行Material/Materials的獲取

在Unity中,如果對Renderer類型調用.material和.materials,那麼Unity就會生成新的材質球實例。其主要影響如下:
通過.material,創建材質實例,並修改屬性的方式實現多樣的渲染效果,時間開銷會較高。這裏可以參見《使用MaterialPropertyBlock來替換Material屬性操作》

使用相同Shader,但因爲Material實例不同的GameObject,所以無法進行合批,導致Draw Call增加,變相造成了CPU耗時的增加。

每次對新的GameObject的Renderer調用.material,都會生成一個新的Material實例,且GameObject銷燬後,Material實例無法自動銷燬,這會對資源管理造成一定的成本,想要處理的話就需要手動調用“UnloadUnusedAssets”來卸載,但這樣就造成了性能開銷;管理不好可能會造成材質球大量冗餘甚至泄露,極端情況下甚至會導致過高的內存。

建議通過主動MaterialPropertyBlock的方式修改材質屬性,或者人爲對有限個材質實例進行管理,效果相同的物體通過sharedMaterial來共用材質實例。


希望以上這些知識點能在實際的開發過程中爲大家帶來幫助。需要說明的是,每一項檢測規則的閾值都可以由開發團隊依據自身項目的實際需求去設置合適的閾值範圍,這也是本地資源檢測的一大特點。同時,也歡迎大家來使用UWA推出的本地資源檢測服務,可幫助大家儘早對項目建立科學的美術規範。

 

萬行代碼屹立不倒,全靠基礎掌握得好!

《場景檢測:面片、光影和物理屬性》
《場景檢測:Audio Listener、RigidBody和Prefab連接》
《場景檢測:霧效、Canvas和碰撞體》

《特效優化2:效果與性能的博弈》
《特效優化:發現絢麗背後的質樸》
《Prefab優化:預製體中的各種細節選擇》

相關推薦
多線程統計 | GOT Online新功能上線

性能黑榜相關閱讀

《那些年給性能埋過的坑,你跳了嗎?》
《那些年給性能埋過的坑,你跳了嗎?(第二彈)》
《掌握了這些規則,你已經戰勝了80%的對手!》

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章