可使用一些相對簡單的技術來減少托管堆分配,
1.集合和陣列重用
使用 C# 的集合類或陣列時,盡可能考慮重用或匯集已分配的集合或陣列,集合類開放了一個 Clear 方法,該方法會消除集合內的值,但不會釋放分配給集合的記憶體,
2.閉包和匿名方法
使用閉包和匿名方法時需要注意兩點,
首先,C# 中的所有方法參考都是參考型別,因此在堆上進行分配,通過將方法參考作為引數傳遞,可以輕松分配臨時記憶體,無論傳遞的方法是匿名方法還是預定義的方法,都會發生此分配,
其次,將匿名方法轉換為閉包后,為了將閉包傳遞給接收閉包的方法,所需的記憶體量將顯著增加,
因為執行閉包需要實體化閉包生成類的副本,并且所有類都是 C# 中的參考型別,所以執行閉包需要在托管堆上分配物件,
通常,請盡可能在 C# 中避免使用閉包,應在性能敏感的代碼中盡可能減少匿名方法和方法參考,尤其是那些每幀都需要執行的代碼中,
3.裝箱 (Boxing)
裝箱是 Unity 專案中最常見的非預期臨時記憶體分配來源之一,只要將值型別的值用作參考型別就會發生裝箱;這種情況最常發生在將原始值型別的變數(例如 int 和 float)傳遞給物件型別的方法時,
C# IDE(集成開發環境)和編譯器通常不會發出關于裝箱的警告,即使導致意外的記憶體分配時也是如此,這是因為 C# 語言的設計理念認為,小型臨時分配可以被分代垃圾回收器和對分配大小敏感的記憶體池有效處理,
雖然 Unity 的分配器實際會使用不同的記憶體池進行小型和大型分配,但 Unity 的垃圾回收器“不是”分代的,因此無法有效清除由裝箱生成的小型、頻繁的臨時分配,
在為 Unity 運行時撰寫 C# 代碼時,應盡可能避免使用裝箱,
4.字典和列舉
裝箱的一個常見原因是使用 enum 型別作為字典的鍵,宣告 enum 會創建一個新值型別,此型別在后臺視為整數,但在編譯時實施型別安全規則,
默認情況下,呼叫 Dictionary.add(key, value) 會導致呼叫 Object.getHashCode(Object),此方法用于獲取字典的鍵的相應哈希代碼,并在所有接受鍵的方法中使用,如:Dictionary.tryGetValue、Dictionary.remove 等,
Object.getHashCode 方法為參考型別,但 enum 值始終為值型別,因此,對于列舉鍵字典,每次方法呼叫都會導致鍵被裝箱至少一次,
5.Foreach 回圈
在 Unity 的 Mono C# 編譯器版本中,使用 foreach 回圈會在每次回圈終止時強制 Unity 將一個值裝箱(__注意:__是在每次整個回圈完整執行完成后將該值裝箱一次,并非在回圈的每次迭代中裝箱一次,因此無論回圈運行兩次還是 200 次,記憶體使用量都保持不變),這是因為 Unity 的 C# 編譯器生成的 IL 會構造一個通用值型別的列舉器來遍歷值集合,
通常,應在 Unity 中避免使用 foreach 回圈,原因不僅是這些回圈會進行裝箱,而且通過列舉器遍歷集合的方法呼叫成本更高,通常比通過 for 或 while 回圈進行的手動迭代慢得多,
請注意,Unity 5.5 中的 C# 編譯器升級版本顯著提高了 Unity 生成 IL 的能力,特別值得注意的是,已從 foreach 回圈中消除裝箱操作,因此,節約了與 foreach 回圈相關的記憶體開銷,但是,由于方法呼叫開銷,與基于陣列的等效代碼相比,CPU 性能差距仍然存在,
6.Unity 陣列值 API
虛陣列分配的一種更有害和更不明顯的原因是重復訪問回傳陣列的 Unity API,回傳陣列的所有 Unity API 每次被訪問時都會創建一個新的陣列副本,在不必要的情況下訪問陣列值 Unity API 是極不適宜的,
例如,下面的代碼在每次回圈迭代時都會虛化創建 vertices 陣列的四個副本,每次訪問 .vertices 屬性時都會發生分配,
for(int i = 0; i < mesh.vertices.Length; i++)
{
float x, y, z;
x = mesh.vertices[i].x;
y = mesh.vertices[i].y;
z = mesh.vertices[i].z;
// ...
DoSomething(x, y, z);
}
通過在進入回圈之前捕獲 vertices 陣列,無論回圈迭代次數是多少,都可以簡單地重構為單個陣列分配:
var vertices = mesh.vertices;
for(int i = 0; i < vertices.Length; i++)
{
float x, y, z;
x = vertices[i].x;
y = vertices[i].y;
z = vertices[i].z;
// ...
DoSomething(x, y, z);
}
雖然訪問一次屬性的 CPU 成本不是很高,但在緊炊訓圈內重復訪問會使得 CPU 性能過熱,此外,重復訪問會導致托管堆出現不必要的擴展,
此問題在移動端極其常見,因為 Input.touches API 的行為與上述類似,專案包含以下類似代碼是極為常見的,此情況下每次訪問 .touches 屬性時都會發生分配,
for ( int i = 0; i < Input.touches.Length; i++ )
{
Touch touch = Input.touches[i];
// …
}
7.空陣列重用
當陣列值方法需要回傳空集時,有些開發團隊更喜歡回傳空陣列而不是 null,這種編碼模式在許多托管語言中很常見,特別是 C# 和 Java,
通常情況下,從方法回傳零長度陣列時,回傳零長度陣列的預分配單例實體比重復創建空陣列要高效得多(5)(__注意:__當然,在回傳陣列后調整陣列大小時是個例外),
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/338145.html
標籤:python
下一篇:由于無法將引數型別“Function”分配給引數型別“voidFunction()”,因此無法創建ontap函式怎么辦?
