了解托管堆
許多 Unity 開發(fā)者面臨的另一個常見問題是托管堆的意外擴(kuò)展。在 Unity 中,托管堆的擴(kuò)展比收縮容易得多。此外,Unity 的垃圾收集策略往往會使內(nèi)存碎片化,因此可能阻止大型堆的收縮。
托管堆的工作原理及其擴(kuò)展原因
“托管堆”是由項目腳本運(yùn)行時(Mono 或 IL2CPP)的內(nèi)存管理器自動管理的一段內(nèi)存。必須在托管堆上分配托管代碼中創(chuàng)建的所有對象(2)(__注意:__嚴(yán)格來說,必須在托管堆上分配所有非 null 引用類型對象和所有裝箱值類型對象)。

在上圖中,白框表示分配給托管堆的內(nèi)存量,而其中的彩色框表示存儲在托管堆的內(nèi)存空間中的數(shù)據(jù)值。當(dāng)需要更多值時,將從托管堆中分配更多空間。
垃圾回收器定期運(yùn)行(3)(__注意:__具體運(yùn)行時間視平臺而定)。這時將掃描堆上的所有對象,將任何不再引用的對象標(biāo)記為刪除。然后會刪除未引用的對象,從而釋放內(nèi)存。
至關(guān)重要的是,Unity 的垃圾收集(使用?Boehm GC 算法)是非分代的,也是非壓縮的?!胺欠执币馕吨?GC 在執(zhí)行每遍收集時必須掃描整個堆,因此隨著堆的擴(kuò)展,其性能會下降。“非壓縮”意味著不會為內(nèi)存中的對象重新分配內(nèi)存地址來消除對象之間的間隙。

上圖為內(nèi)存碎片化示例。釋放對象時,將釋放其內(nèi)存。但是,釋放的空間不會整合成為整個“可用內(nèi)存”池的一部分。位于釋放的對象兩側(cè)的對象可能仍在使用中。因此,釋放的空間成為其他內(nèi)存段之間的“間隙”(該間隙由上圖中的紅色圓圈指示)。因此,新釋放的空間僅可用于存儲與釋放相同大小或更小的對象的數(shù)據(jù)。
分配對象時,請注意對象在內(nèi)存空間中的分配地址必須始終為連續(xù)空間塊。
這導(dǎo)致了內(nèi)存碎片化這個核心問題:雖然堆中的可用空間總量可能很大,但是可能其中的部分或全部的可分配空間對象之間存在小的“間隙”。這種情況下,即使可用空間總量高于要分配的空間量,托管堆可能也找不到足夠大的連續(xù)內(nèi)存塊來滿足該分配需求。

但是,如果分配了大型對象,卻沒有足夠的連續(xù)可用空間來容納該對象(如上所示),Unity 內(nèi)存管理器將執(zhí)行兩個操作。
首先,如果垃圾回收器尚未運(yùn)行,則運(yùn)行垃圾回收器。此工具會嘗試釋放足夠的空間來滿足分配請求。
如果在 GC 運(yùn)行后,仍然沒有足夠的連續(xù)空間來滿足請求的內(nèi)存量,則必須擴(kuò)展堆。堆的具體擴(kuò)展量視平臺而定;但是,大多數(shù) Unity 平臺會使托管堆的大小翻倍。
堆的主要問題
托管堆擴(kuò)展方面的核心問題有兩個:
Unity 在擴(kuò)展托管堆后不會經(jīng)常釋放分配給托管堆的內(nèi)存頁;它會樂觀地保留擴(kuò)展后的堆,即使該堆的大部分為空時也如此。這是為了防止再次發(fā)生大量分配時需要重新擴(kuò)展堆。
在大多數(shù)平臺上,Unity 最終會將托管堆的空置部分使用的頁面釋放回操作系統(tǒng)。發(fā)生此行為的間隔時間是不確定的,因此不要指望靠這種方法釋放內(nèi)存。
托管堆使用的地址空間始終不會歸還給操作系統(tǒng)。
對于 32 位程序,如果托管堆多次擴(kuò)展和收縮,則可能導(dǎo)致地址空間耗盡。如果一個程序的可用內(nèi)存地址空間已耗盡,操作系統(tǒng)將終止該程序。
對于 64 位程序而言,地址空間足夠大到可以運(yùn)行時間超過人類平均壽命的程序,因此地址空間耗盡的這種情況極幾乎不可能發(fā)生。
臨時分配
許多 Unity 項目在每幀都有幾十或幾百 KB 的臨時數(shù)據(jù)分配給托管堆。這種情況通常對項目的性能極為不利。請考慮以下數(shù)學(xué)計算:
如果一個程序每幀分配一千字節(jié) (1 KB) 的臨時內(nèi)存,并且以每秒 60 幀的速率運(yùn)行,那么它必須每秒分配 60 KB 的臨時內(nèi)存。在一分鐘內(nèi),這會在內(nèi)存中增加 3.6 MB 的垃圾。每秒調(diào)用一次垃圾回收器可能會對性能產(chǎn)生不利影響,但對于內(nèi)存不足的設(shè)備而言每分鐘分配 3.6 MB 的內(nèi)存是個問題。
此外,請考慮加載操作。如果在大型資源加載操作期間生成了大量臨時對象,并且對這些對象的引用一直持續(xù)到操作完成,則垃圾回收器無法釋放這些臨時對象,并且托管堆需要進(jìn)行擴(kuò)展,即使它包含的許多對象將在不久后釋放也是如此。

跟蹤托管內(nèi)存分配情況相對簡單。在 Unity 的 CPU 性能分析器中,Overview 表有一個“GC Alloc”列。此列顯示了在特定幀中的托管堆上分配的字節(jié)數(shù)(4)(__注意:__這與給定幀期間臨時分配的字節(jié)數(shù)不同。性能分析器會顯示特定幀中分配的字節(jié)數(shù),不考慮在后續(xù)幀中是否重用了部分/全部已分配的內(nèi)存)。啟用“Deep Profiling”選項后,可以跟蹤執(zhí)行這些分配的方法。
Unity Profiler 不會跟蹤在主線程之外發(fā)生的分配。因此,“GC Alloc”列不能用于統(tǒng)計用戶創(chuàng)建的線程中發(fā)生的托管分配。請將代碼執(zhí)行從單獨(dú)線程切換到主線程以進(jìn)行調(diào)試,或使用?BeginThreadProfiling?API 在時間軸性能分析器 (Timeline Profiler) 中顯示例程。
務(wù)必使用目標(biāo)設(shè)備上的開發(fā)版來分析托管分配。
請注意,某些腳本方法在 Editor 中運(yùn)行時會產(chǎn)生分配內(nèi)存,但在構(gòu)建項目后不會產(chǎn)生分配內(nèi)存。GetComponent
?是最常見的示例;此方法始終在 Editor 中執(zhí)行時分配內(nèi)存,而不會在已構(gòu)建的項目中分配內(nèi)存。
通常,強(qiáng)烈建議所有開發(fā)人員在項目處于交互狀態(tài)時最大限度減少托管堆內(nèi)存分配。非交互操作(例如場景加載)期間的內(nèi)存分配很少產(chǎn)生問題。
適用于 Visual Studio 的?Jetbrains Resharper 插件可以幫助找到代碼中的內(nèi)存分配。
使用 Unity 的深度性能分析 (Deep Profile)?模式可找到托管分配的具體原因。在深度性能分析模式下,所有方法調(diào)用都是單獨(dú)記錄的,可更清晰地查看方法調(diào)用樹中發(fā)生托管分配的位置。請注意,深度性能分析模式不僅可在 Editor 中使用,還可借助命令行參數(shù)?-deepprofiling
?在 Android 和桌面平臺上運(yùn)行。Deep Profiler 按鈕在性能分析期間保持灰色。
基本的內(nèi)存節(jié)省方法
可使用一些相對簡單的技術(shù)來減少托管堆分配。
集合和數(shù)組重用
使用 C# 的集合類或數(shù)組時,盡可能考慮重用或匯集已分配的集合或數(shù)組。集合類開放了一個?Clear?方法,該方法會消除集合內(nèi)的值,但不會釋放分配給集合的內(nèi)存。
void Update() {
? ?
List<float> nearestNeighbors = new List<float>();
? findDistancesToNearestNeighbors(nearestNeighbors);
? ?
nearestNeighbors.Sort();
? ?
// … 以某種方式使用排序列表 …?
}
在為復(fù)雜計算分配臨時“helper”集合時,這尤其有用。下面的代碼是一個非常簡單的示例:
在此示例中,為了收集一組數(shù)據(jù)點(diǎn),每幀都為?nearestNeighbors
?List(列表)分配一次內(nèi)存。將此 List 從方法中提升到包含類中是非常簡單的,這樣做避免了每幀都為新 List 分配內(nèi)存:
List<float> m_NearestNeighbors = new List<float>();?
void Update() {
? ?
m_NearestNeighbors.Clear();
??
findDistancesToNearestNeighbors(NearestNeighbors);
? ?
m_NearestNeighbors.Sort();
? ?
// … 以某種方式使用排序列表 …?
}
在此版本中,List 的內(nèi)存被保留并在多個幀之間重用。僅在 List 需要擴(kuò)展時才分配新內(nèi)存。
閉包和匿名方法
使用閉包和匿名方法時需要注意兩點(diǎn)。
首先,C# 中的所有方法引用都是引用類型,因此在堆上進(jìn)行分配。通過將方法引用作為參數(shù)傳遞,可以輕松分配臨時內(nèi)存。無論傳遞的方法是匿名方法還是預(yù)定義的方法,都會發(fā)生此分配。
其次,將匿名方法轉(zhuǎn)換為閉包后,為了將閉包傳遞給接收閉包的方法,所需的內(nèi)存量將顯著增加。
請參考以下代碼:
List<float> listOfNumbers = createListOfRandomNumbers();?
listOfNumbers.Sort( (x, y) =>
(int)x.CompareTo((int)(y/2))
);
這段代碼使用簡單的匿名方法來控制在第一行創(chuàng)建的數(shù)字列表的排序順序。但是,如果程序員希望使該代碼段可重用,很容易想到將常量?2
?替換為局部作用域內(nèi)的變量,如下所示:
List<float> listOfNumbers = createListOfRandomNumbers();?
int desiredDivisor = getDesiredDivisor();?
listOfNumbers.Sort( (x, y) =>
(int)x.CompareTo((int)(y/desiredDivisor))
);
匿名方法現(xiàn)在要求該方法能夠訪問方法作用域之外的變量狀態(tài),因此已成為閉包。必須以某種方式將?desiredDivisor
?變量傳遞給閉包,以便閉包的實(shí)際代碼可以使用該變量。
為此,C# 將生成一個匿名類,該類可保存閉包所需的外部作用域變量。當(dāng)閉包傳遞給?Sort
?方法時,將實(shí)例化此類的副本,并用?desiredDivisor
?整數(shù)的值初始化該副本。
因為執(zhí)行閉包需要實(shí)例化閉包生成類的副本,并且所有類都是 C# 中的引用類型,所以執(zhí)行閉包需要在托管堆上分配對象。
通常,請盡可能在 C# 中避免使用閉包。應(yīng)在性能敏感的代碼中盡可能減少匿名方法和方法引用,尤其是那些每幀都需要執(zhí)行的代碼中。
IL2CPP 下的匿名方法
目前,通過查看 IL2CPP 所生成的代碼得知,對System.Function
?類型變量的聲明和賦值將會分配一個新對象。無論變量是顯式的(在方法/類中聲明)還是隱式的(聲明為另一個方法的參數(shù)),都是如此。
因此,使用 IL2CPP 腳本后端下的匿名方法必定會分配托管內(nèi)存。在 Mono 腳本后端下則不是這種情況。
此外,由于方法參數(shù)的聲明方式不同,將導(dǎo)致IL2CPP 顯示出托管內(nèi)存分配量產(chǎn)生巨大差異。正如預(yù)期的那樣,閉包的每次調(diào)用會消耗最多的內(nèi)存。
預(yù)定義的方法在 IL2CPP 腳本后端下作為參數(shù)傳遞時,其__分配的內(nèi)存幾乎與閉包一樣多__,但這不是很直觀。匿名方法在堆上生成最少量的臨時垃圾(一個或多個數(shù)量級)。
因此,如果打算在 IL2CPP 腳本后端上發(fā)布項目,有三個主要建議:
最好選擇不需要將方法作為參數(shù)傳遞的編碼風(fēng)格。
當(dāng)不可避免時,最好選擇匿名方法而不是預(yù)定義方法。
無論腳本后端為何,都要避免使用閉包。
裝箱 (Boxing)
裝箱是 Unity 項目中最常見的非預(yù)期臨時內(nèi)存分配來源之一。只要將值類型的值用作引用類型就會發(fā)生裝箱;這種情況最常發(fā)生在將原始值類型的變量(例如?int
?和?float
)傳遞給對象類型的方法時。
在下面非常簡單的示例中,對?x?中的整數(shù)進(jìn)行了裝箱以便傳遞給?object.Equals
?方法,因為?object
?上的?Equals
?方法要求將?object
?作為參數(shù)傳遞給它。
int x = 1;
object y = new object();
y.Equals(x);
C# IDE(集成開發(fā)環(huán)境)和編譯器通常不會發(fā)出關(guān)于裝箱的警告,即使導(dǎo)致意外的內(nèi)存分配時也是如此。這是因為 C# 語言的設(shè)計理念認(rèn)為,小型臨時分配可以被分代垃圾回收器和對分配大小敏感的內(nèi)存池有效處理。
雖然 Unity 的分配器實(shí)際會使用不同的內(nèi)存池進(jìn)行小型和大型分配,但 Unity 的垃圾回收器“不是”分代的,因此無法有效清除由裝箱生成的小型、頻繁的臨時分配。
在為 Unity 運(yùn)行時編寫 C# 代碼時,應(yīng)盡可能避免使用裝箱。
識別裝箱
裝箱在 CPU 跟蹤中顯示為對某幾種特定方法的調(diào)用,具體形式取決于使用的腳本后端。這些調(diào)用通常采用以下形式之一,其中?<some class>
?是其他類或結(jié)構(gòu)的名稱,而?...
?是一些參數(shù):
<some class>::Box(…)
Box(…)
<some class>_Box(…)
也可以通過搜索反編譯器或 IL 查看器(例如 ReSharper 中內(nèi)置的 IL 查看器工具或 dotPeek 反編譯器)的輸出來定位裝箱。IL 指令為“box”。
字典和枚舉
裝箱的一個常見原因是使用?enum
?類型作為字典的鍵。聲明?enum
?會創(chuàng)建一個新值類型,此類型在后臺視為整數(shù),但在編譯時實(shí)施類型安全規(guī)則。
默認(rèn)情況下,調(diào)用?Dictionary.add(key, value)
?會導(dǎo)致調(diào)用?Object.getHashCode(Object)
。此方法用于獲取字典的鍵的相應(yīng)哈希代碼,并在所有接受鍵的方法中使用,如:Dictionary.tryGetValue, Dictionary.remove
?等。
Object.getHashCode
?方法為引用類型,但?enum
?值始終為值類型。因此,對于枚舉鍵字典,每次方法調(diào)用都會導(dǎo)致鍵被裝箱至少一次。
以下代碼片段展示的一個簡單示例說明了此裝箱問題:
enum MyEnum { a, b, c };?
var myDictionary =
new Dictionary<MyEnum, object>();?
myDictionary.Add(MyEnum.a, new object());
要解決此問題,則需要編寫一個實(shí)現(xiàn)?IEqualityComparer
?接口的自定義類,并將該類的實(shí)例指定為字典的比較器(__注意:__此對象通常是無狀態(tài)的,因此可與不同的字典實(shí)例一起重復(fù)使用以節(jié)省內(nèi)存)。
以下是上述代碼片段 IEqualityComparer 的簡單示例。
public class MyEnumComparer : IEqualityComparer<MyEnum> {
? ?
public bool Equals(MyEnum x, MyEnum y) {
? ? ? ?
return x == y;
? ?
}
? ?
public int GetHashCode(MyEnum x) {
? ? ? ?
return (int)x;
? ?
}
}
可將上述類的實(shí)例傳遞給字典的構(gòu)造函數(shù)。
Foreach 循環(huán)
在 Unity 的 Mono C# 編譯器版本中,使用?foreach
?循環(huán)會在每次循環(huán)終止時強(qiáng)制 Unity 將一個值裝箱(__注意:__是在每次整個循環(huán)完整執(zhí)行完成后將該值裝箱一次,并非在循環(huán)的每次迭代中裝箱一次,因此無論循環(huán)運(yùn)行兩次還是 200 次,內(nèi)存使用量都保持不變)。這是因為 Unity 的 C# 編譯器生成的 IL 會構(gòu)造一個通用值類型的枚舉器來遍歷值集合。
此枚舉器將實(shí)現(xiàn)?IDisposable
?接口;當(dāng)循環(huán)終止時必須調(diào)用該接口。但是,在值類型的對象(例如結(jié)構(gòu)和枚舉器)上調(diào)用接口方法需要將它們裝箱。
請參考下面非常簡單的示例代碼:
int accum = 0;?
foreach(int x in myList) {
? ?
accum += x;?
}
以上代碼通過 Unity 的 C# 編譯器運(yùn)行后將生成以下中間語言:
.method private hidebysig instance void
? ? ??
ILForeach() cil managed
? ??
{
? ? ??
.maxstack 8
? ? ?
.locals init (
? ? ? ??
[0] int32 num,
? ? ? ??
[1] int32 current,
? ? ? ??
[2] valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<int32> V_2
)
??
// [67 5 - 67 16]
? ? ?
IL_0000: ldc.i4.0 ? ? ? ? ??
IL_0001: stloc.0 ? ? ?// num ? ? ??
// [68 5 - 68 74] ? ? ??
IL_0002: ldarg.0 ? ? ?// this ? ? ??
IL_0003: ldfld ? ? ? ?class [mscorlib]System.Collections.Generic.List`1<int32> test::myList ? ? ? IL_0008: callvirt ? ? instance valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<!0/*int32*/> class [mscorlib]System.Collections.Generic.List`1<int32>::GetEnumerator() ? ? ??
IL_000d: stloc.2 ? ? ?// V_2 ? ? ? .try ? ? ??
{
IL_000e: br ? ? ? ? ? IL_001f ? ? ??
// [72 9 - 72 41] ? ? ? ??
IL_0013: ldloca.s ? ? V_2 ? ? ? ??
IL_0015: call ? ? ? ? instance !0/*int32*/ valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<int32>::get_Current() ? ? ? ??
IL_001a: stloc.1 ? ? ?// current ? ? ??
// [73 9 - 73 23] ? ? ? ??
IL_001b: ldloc.0 ? ? ?// num ? ? ? ??
IL_001c: ldloc.1 ? ? ?// current ? ? ? ??
IL_001d: add ? ? ? ? ? ? ? ? ??
IL_001e: stloc.0 ? ? ?// num ? ? ??
// [70 7 - 70 36] ? ? ? ??
IL_001f: ldloca.s ? ? V_2 ? ? ? ??
IL_0021: call ? ? ? ? instance bool valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<int32>::MoveNext() ? ? ? ??
IL_0026: brtrue ? ? ? IL_0013 ? ? ? ??
IL_002b: leave ? ? ? ?IL_003c ? ? ??
}?
// .try 結(jié)束 ? ? ??
finally ? ? ??
{ ? ? ? ??
IL_0030: ldloc.2 ? ? ?// V_2 ? ? ? ??
IL_0031: box ? ? ? ? ?valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<int32> ? ? ? ??
IL_0036: callvirt ? ? instance void [mscorlib]System.IDisposable::Dispose() ? ? ? ??
IL_003b: endfinally ? ? ? ??
} // finally 結(jié)束 ? ? ??
IL_003c: ret ? ? ? ? ? ? ??
} // 方法 test::ILForeach結(jié)束 ??
} // test 類結(jié)束
最相關(guān)的代碼是靠近底部的?__finally { … }__
?代碼塊。callvirt
?指令在調(diào)用?IDisposable.Dispose
?方法之前先發(fā)現(xiàn)該方法在內(nèi)存中的位置,并要求將枚舉器裝箱。
通常,應(yīng)在 Unity 中避免使用?foreach
?循環(huán)。原因不僅是這些循環(huán)會進(jìn)行裝箱,而且通過枚舉器遍歷集合的方法調(diào)用成本更高,通常比通過?for
?或?while
?循環(huán)進(jìn)行的手動迭代慢得多。
請注意,Unity 5.5 中的 C# 編譯器升級版本顯著提高了 Unity 生成 IL 的能力。特別值得注意的是,已從?foreach
?循環(huán)中消除裝箱操作。因此,節(jié)約了與?foreach
?循環(huán)相關(guān)的內(nèi)存開銷。但是,由于方法調(diào)用開銷,與基于數(shù)組的等效代碼相比,CPU 性能差距仍然存在。
Unity 數(shù)組值 API
虛數(shù)組分配的一種更有害和更不明顯的原因是重復(fù)訪問返回數(shù)組的 Unity API。返回數(shù)組的所有 Unity API 每次被訪問時都會創(chuàng)建一個新的數(shù)組副本。在不必要的情況下訪問數(shù)組值 Unity API 是極不適宜的。
例如,下面的代碼在每次循環(huán)迭代時都會虛化創(chuàng)建?vertices
?數(shù)組的四個副本。每次訪問?.vertices
?屬性時都會發(fā)生分配。
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);?
}
通過在進(jìn)入循環(huán)之前捕獲?vertices
?數(shù)組,無論循環(huán)迭代次數(shù)是多少,都可以簡單地重構(gòu)為單個數(shù)組分配:
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 成本不是很高,但在緊湊循環(huán)內(nèi)重復(fù)訪問會使得 CPU 性能過熱。此外,重復(fù)訪問會導(dǎo)致托管堆出現(xiàn)不必要的擴(kuò)展。
此問題在移動端極其常見,因為?Input.touches
?API 的行為與上述類似。項目包含以下類似代碼是極為常見的,此情況下每次訪問?.touches
?屬性時都會發(fā)生分配。
for ( int i = 0; i < Input.touches.Length; i++ )
{
Touch touch = Input.touches[i];
? ?
// …
}
當(dāng)然,通過將數(shù)組分配從循環(huán)條件中提升出來,可輕松改善該問題:
Touch[] touches = Input.touches;?
for ( int i = 0; i < touches.Length; i++ )?
{
Touch touch = touches[i]; ??
// … }
但是,現(xiàn)在有許多 Unity API 的版本不會導(dǎo)致內(nèi)存分配。如果能使用這些版本時,請盡量選擇這種版本。
int touchCount = Input.touchCount;?
for ( int i = 0; i < touchCount; i++ ){
Touch touch = Input.GetTouch(i);
??
// …
}
將上面的示例轉(zhuǎn)換為無分配的 Touch API 很簡單:
請注意,為了節(jié)省調(diào)用屬性的?get
?方法的 CPU 成本,屬性訪問 (Input.touchCount
) 仍然保持在循環(huán)條件之外。
空數(shù)組重用
當(dāng)數(shù)組值方法需要返回空集時,有些開發(fā)團(tuán)隊更喜歡返回空數(shù)組而不是?null
。這種編碼模式在許多托管語言中很常見,特別是 C# 和 Java。
通常情況下,從方法返回零長度數(shù)組時,返回零長度數(shù)組的預(yù)分配單例實(shí)例比重復(fù)創(chuàng)建空數(shù)組要高效得多(5)(__注意:__當(dāng)然,在返回數(shù)組后調(diào)整數(shù)組大小時是個例外)。
腳注
(1)?這是因為大多數(shù)平臺上從 GPU 內(nèi)存回讀的速度極慢。將紋理從 GPU 內(nèi)存讀入臨時緩沖區(qū)以供 CPU 代碼(例如?
Texture.GetPixel
)使用將是非常不高效的。(2)?嚴(yán)格來說,必須在托管堆上分配所有非 null 引用類型對象和所有裝箱值類型對象。
(3)?具體運(yùn)行時間視平臺而定。
(4)?注意,這與給定幀期間臨時分配的字節(jié)數(shù)不同。性能分析器會顯示特定幀中分配的字節(jié)數(shù),不考慮在后續(xù)幀中是否重用了部分/全部已分配的內(nèi)存。
(5)?當(dāng)然,在返回數(shù)組后調(diào)整數(shù)組大小時是個例外。