關於C#記憶體管理那些事。

N 人看过

You can also see my post in Bahamut.

使用Unity製作遊戲也好一陣子,

途中踩過不少坑都是對於Unity預設值與C#程式語言的設計上的不熟悉,

關於Unity預設值的部分,達哥在今年的TGDF上的簡報讓我獲益良多,大家都該去看看。

這次想探討的是關於C#程式語言的設計方面,

特別是相對於C/C++這些低階語言來說,C#/Java的記憶體分配像是一個巨大的黑盒子,

運作起來還蠻順利的,但是時不時就會像樂高一樣踩到腳(而且很痛),

這篇就來記錄一下自己學到的東西,順便討論裝箱(Boxing)跟C#的記憶體運作吧!

一、C#的記憶體分配

由於看到文章的朋友,可能不一定對程式語言的一些處理非常孰悉,

所以在這邊先簡單整理關於C#記憶體分配的一些規則。

對C#來說,一個Class分成三種類型:

  1. Value Type 2. Reference Type 3. Pointer Type

其中第三項要開啟unsafe才能夠使用,而Unity的預設值是關閉的,所以可以先不理它。

前兩項Value Type跟Reference Type,在MSDN的官方說明上是這樣的:

Value types are either stack-allocated or allocated inline in a structure.
Reference types are heap-allocated

可以了解到,

對於Value Type來說,一般是存放在STACK段,做為區域變數使用。

對於Reference Type來說,一般是存放在HEAP段,由C#的Garbage Collection控管。

(當然這樣簡單的分類未必完全正確,因為他們其實存在一些實作細節的不同)

簡單來說:

Value Type就是一般常用的int, float, char, struct, enum這種簡單的資料型態,

他們在一般情況通常是區域變數,也就是用完就丟的,對記憶體的消耗也不會太大。

Reference Type就是利用class包裝起來的資料了,他們會被整包放到動態記憶體裡面,

如果不特別處理,就會持續占用記憶體,當相關的程式使用完這些變數,

C#就會利用Garbage Collection的方法,回收這些用不到的記憶體,

在Collect的當下會消耗較多CPU資源,甚至在手機上有可能會導致掉幀的情況,

所以我們一般會希望迴避這種動態記憶體的分配能夠有所節制,

也是前幾篇文章所提到的物件池存在的一部份原因。

二、萬惡的Boxing跟Unboxing

介紹完兩種C#的記憶體分配方式之後,再回頭討論標題的Boxing究竟是什麼意思吧。

在寫遊戲程式的時候,常常會遇到一種狀況:

「需要把現在的數值記錄下來,做為某種用途」

這種情況,我們常常會宣告一個class,像是下面這種存檔用的資訊:

sd

在使用上,我們通常會寫成:

sd

這種形式,此時原本存在區域變數的level跟health,就需要複製一份到動態變數區,

並且包裝成SaveFile的形式一直存在於動態變數區,直到它被使用完畢,被GC給回收去。

這樣從區域變數(Value Type)被打包到動態變數區段(HEAP段),就被稱為Boxing(裝箱)

反過來從動態變數區段擷取成區域變數(Value Type)就被稱為Unboxing(拆箱)

三、總結

其實Boxing跟Unboxing在寫程式的時候幾乎是難以完全避免的,

我們只要記得盡量少使用裝拆箱,以及在使用ArrayList, HashTable這種

一放進去就會被轉型成object type的自動裝箱結構時,要務必小心使用。

另外就是盡量在不忙碌的時候(打開UI選單、暫停遊戲、或讀取關卡時),

手動呼叫System.GC.Collect()進行手動GC,勢必可以稍微減少GC回收對效能造成的影響囉!

參考資料:
https://docs.microsoft.com/zh-tw/dotnet/csharp/programming-guide/types/boxing-and-unboxing
https://docs.microsoft.com/zh-tw/dotnet/standard/garbage-collection/fundamentals
https://docs.microsoft.com/zh-tw/dotnet/api/system.valuetype?view=netframework-4.8
http://dev.yesky.com/msdn/359/3486359.shtml