struct實例字段的內存佈局(Layout)和大小

背景

在C/C++中,struct類型中的成員的一旦聲明,則實例中成員在內存中的佈局 (Layout) 順序就定下來了,即與成員聲明的順序相同,並且在默認情況下總是按照結構中佔用空間最大的成員進行對齊(Align);當然我們也可以通過設置或編碼來設置內存對齊的方式。

然而在 .net 託管環境中,CLR 提供了更自由的方式來控制 struct 中 Layout:我們可以在定義 struct時,在 struct 上運用 StructLayoutAttribute 特性來控制成員的內存佈局。默認情況下,struct 實例中的字段在棧上的佈局 (Layout) 順序與聲明中的順序相同,即在 struct 上運用 [StructLayoutAttribute(LayoutKind.Sequential)]特性,這樣做的原因是結構常用於和非託管代碼交互的情形。但是對於引用類型,默認的則是 LayoutKind.Auto

如果我們的值類型不會與非託管代碼互操作,就應該覆蓋 C# 編譯器的默認設定。 LayoutKind 除了 Sequential 成員之外,還有兩個成員 AutoExplicit,給 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.ce.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)。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章