Effective C# 09.減少 boxing 與 unboxing Effective C# 09.減少 boxing 與 unboxing (Minimize Boxing and Unboxing)

Published on Wednesday, October 2, 2024

這個做法在討論 boxing 與 unboxing 的運作方式與背後理論。 首先要先複習 Value TypeReference Type 兩者關鍵的區別,Value Type 的複製是 Copy-by-value,Reference Type 的複製是 Copy-by-reference。

建立 Reference Type 時會有以下行為會影響效能:

  1. 由 Managed Heap 管理及分配記憶體。
  2. 每個 Heap 上分配的物件會有幾個額外的 members 需要初始化。
  3. 物件內含的 fields 會初始化為 0。
  4. Managed Heap 為物件分配記憶體時可能會強制執行 GC。

試想如果 .NET 中所有物件都是 Reference Type 那麼每次使用例如 int 背後都要進行一次內存分配那效能會有多差。

因此提供了 Value Type 這個更輕量級的類型,Value Type 是由 Thread Stack 管理及分配記憶體,並且特色是 一個 Value Type 的變數儲存的是實際的值而並不是記憶體位置,因此讀取這類型的變數不需要 dereference,並且 不受 GC 管控,所以可以大大降低 Managed Heap 的壓力與 GC 的次數。

Boxing

但並不是所有方法都有多載的版本能夠接受 Value Type,例如 string.Concat 這個把字串相連在一起的方法就是其中一個例子。

以這段程式為例子 string.Concat 一般來說傳入的參數為 string 類型,很明顯我們的 int 類型它是不會接受的。

void Main()
{
	int year = 2024;
	Console.WriteLine(string.Concat(year));
}

所以這裡我們有兩個選擇:

  1. 將呼叫 ToString 方法將 year 轉換成 string 類型。
  2. 將 year 轉換成 object 類型,使用 object 多載的版本。

而將 Value Type 轉換成 object 類型的操作就叫做 boxing,從 object 轉回當初的類型就叫做 unboxing

可以從 IL 碼中看出 IL_0008 就是一個 boxing 的操作。

IL_0000	nop	
IL_0001	ldc.i4	E8 07 00 00  // 2024
IL_0006	stloc.0	   // year
IL_0007	ldloc.0	   // year
IL_0008	box	Int32
IL_000D	call	String.Concat(Object)
IL_0012	call	Console.WriteLine(String)
IL_0017	nop	
IL_0018	ret	

當一個 Value Type 裝箱成 object 類型就正式變成一個 Reference Type, 從上面整理的影響效能的清單中可以得出 boxing 就是一個影響效能的操作。

從 IL 碼中可以得知 Stack 目前有分配 int32 也就是 4 個 Byte 的記憶體來記錄 year 變數, 接下來 boxing 會在 Heap 中建立一個新的箱子並把 year 變數資料複製到這個箱子裡面,之後把這個箱子的參考位址記錄到 Stack 裡面。 也同樣得出 boxing 是一個影響效能的操作。

Unboxing

接下來看 unboxing 也就是把 object 轉回原本的類型,有一個需要注意的特性那就是不可以轉型到其他類型,不是原本的型別會報 InvalidCastException 錯誤。

void Main()
{
	object year = (object)2024;
	long nextYear = (long)year + 1;
}

unboxing 會將原本 Heap 中的資料複製回 Stack,所以可以想像出一次裝箱拆箱會占用許多不必要使用的記憶體。

void Main()
{
	object year = (object)2024;
	int nextYear = (int)year + 1;
}

從 IL 碼中的 IL_000D 可以看到 unbox 的操作。

IL_0000	nop	
IL_0001	ldc.i4	E8 07 00 00  // 2024
IL_0006	box	Int32
IL_000B	stloc.0	   // year
IL_000C	ldloc.0	   // year
IL_000D	unbox.any	Int32
IL_0012	ldc.i4.1	
IL_0013	add	
IL_0014	stloc.1	   // nextYear
IL_0015	ret	

Summary

從這個做法中可以學到 Value TypeReference Type 的區別還有 boxingunboxing 處理的流程與它的缺點, 其中最不容易發現的就是隱含轉型的裝箱,也就是上面 string.Concat 的例子,這種背後自動轉型的很容易沒注意到就會影響到效能, 所以當不確定是否寫法中有 boxingunboxing 的行為時,最好是搭配反編譯軟體查看 IL 碼才是最準確的。