背景
在C/C++中,struct類型中的成員的一旦聲明,則實例中成員在內存中的佈局 (Layout) 順序就定下來了,即與成員聲明的順序相同,並且在默認情況下總是按照結構中佔用空間最大的成員進行對齊(Align);當然我們也可以通過設置或編碼來設置內存對齊的方式。
然而在 .net 託管環境中,CLR 提供了更自由的方式來控制 struct 中 Layout:我們可以在定義 struct時,在 struct 上運用 StructLayoutAttribute
特性來控制成員的內存佈局。默認情況下,struct 實例中的字段在棧上的佈局 (Layout) 順序與聲明中的順序相同,即在 struct 上運用 [StructLayoutAttribute(LayoutKind.Sequential)]
特性,這樣做的原因是結構常用於和非託管代碼交互的情形。但是對於引用類型,默認的則是 LayoutKind.Auto
。
如果我們的值類型不會與非託管代碼互操作,就應該覆蓋 C# 編譯器的默認設定。 LayoutKind
除了 Sequential
成員之外,還有兩個成員 Auto
和Explicit
,給 StructLayoutAttribute
傳入 LayoutKind.Auto
可以讓 CLR 按照自己選擇的最優方式來排列實例中的字段;傳入 LayoutKind.Explicit
可以使字段按照我們的在字段上設定的 FieldOffset
來更靈活的設置字段排序方式,但這種方式也挺危險的,如果設置錯誤後果將會比較嚴重。
Struct 的內存佈局
struct 的最小大小爲 1 Byte:
struct EmptyStruct {};
// sizeof (EmptyStruct) = 1
內存佈局流程
一個 struct 的內存佈局流程可以簡化爲下面幾步:
- struct 放到地址0上
- struct 的所有成員順序地依次放置到自己的偏移位置上
- 所有成員放置完畢,對struct的內容大小(最後一個成員的終了位置)進行對齊,計算出新大小
流程中涉及到兩點:
- 如何確定一個成員的放置起始位置
- 如何最後對struct的內容大小進行對齊
成員的起始位置
當放置完上一個成員之後,對接下來的成員放置位置是有要求的,要求這個位置偏移(對於struct的起始位置)必須是這個成員的內部最大類型的大小的倍數。
在上個放置成員尾部和當前成員起始位置之間如果有間隙,那麼編譯器會填充無效字節。
然後就可以把當前成員放置進去,放入的大小爲當前成員的總大小。
struct 的大小對齊
成員都放置完畢之後,struct的內容大小有個規則,必須是內部最大類型的大小的倍數。沒錯,就是上面那個 maxInnerUnitSize。
然後在最後一個成員末尾到struct的新大小末尾會填充字節(如果有空隙的話)。
案例
下面就看幾個示例,算下四個struct各佔多少Byte ?
1. [StructLayout(LayoutKind.Sequential)]
struct StructDeft//C#編譯器會自動在上面運用[StructLayout(LayoutKind.Sequential)]
{
bool i; //1Byte
double c;//8Byte
bool b; //1Byte
}
sizeof(StructDeft)
得到的結果是 24 Byte!啊哈,本身只有 10 Byte 的數據卻佔有了 24 Byte 的內存,這是因爲默認(LayoutKind.Sequential
)情況下,CLR 對 struct 的 Layout 的處理方法與 C/C++ 中默認的處理方式相同(8+8+8=24),即按照結構中佔用空間最大的成員進行對齊(Align)。10 Byte 的數據卻佔有了 24 Byte,嚴重地浪費了內存,所以如果我們正在創建一個與非託管代碼沒有任何互操作的 struct類型,最好還是不要使用默認的 StructLayoutAttribute(LayoutKind.Sequential)
特性。
2. [StructLayout(LayoutKind.Explicit)]
[StructLayout(LayoutKind.Explicit)]
struct BadStruct
{
[FieldOffset(0)]
public bool i; //1Byte
[FieldOffset(0)]
public double c;//8byte
[FieldOffset(0)]
public bool b; //1Byte
}
sizeof(BadStruct)
得到的結果是 8 Byte,得出的基數 8 顯示 CLR 並沒對結構體進行任何內存對齊(Align);本身要佔有 10Byte 的數據卻只佔了 8Byte,顯然有些數據被丟失了,這也正是我給 struct 取BadStruct
作爲名字的原因。如果在 struct 上運用了 [StructLayout(LayoutKind.Explicit)]
,計算 FieldOffset
一定要小心,例如我們使用上面 BadStruct
來進行下面的測試:
StructExpt e = new StructExpt();
e.c = 0;
e.i = true;
Console.WriteLine(e.c);
輸出的結果不再是 0 了,而是 4.94065645841247E-324 ,這是因爲 e.c
和e.i
共享同一個 byte,執行e.i = true;
時也改變了 e.c
,CPU在按照浮點數的格式解析 e.c
時就得到了這個結果。所以在運用 LayoutKind.Explicit
時千萬別吧 FieldOffset
算錯了:)
3. [StructLayout(LayoutKind.Auto)]
sizeof(StructAuto)
得到的結果是12 Byte。下面來測試下這 StructAuto
的三個字段是如何擺放的:
unsafe
{
StructAuto s = new StructAuto();
Console.WriteLine(string.Format("i:{0}", (int)&(s.i)));
Console.WriteLine(string.Format("c:{0}", (int)&(s.c)));
Console.WriteLine(string.Format("b:{0}", (int)&(s.b)));
}
// 測試結果:
i:1242180
c:1242172
b:1242181
即CLR會對結構體中的字段順序進行調整,將 i 調到 c 之後,使得 StructAuto
的實例 s 佔有儘可能少的內存,並進行 4byte
的內存對齊(Align),字段順序調整結果如下圖所示:
結論
- 默認
(LayoutKind.Sequential)
情況下,CLR 對 struct 的 Layout 的處理方法與 C/C++ 中默認的處理方式相同,即按照結構中佔用空間最大的成員進行對齊(Align); - 使用
LayoutKind.Explicit
的情況下,CLR 不對結構體進行任何內存對齊(Align),而且我們要小心就是FieldOffset
; - 使用
LayoutKind.Auto
的情況下,CLR 會對結構體中的字段順序進行調整,使實例佔有儘可能少的內存,並進行 4 Byte 的內存對齊(Align)。