前言
大部分情況下我們并不需要關心結構體字段的內存布局,但是在一些特殊情況下,比如性能優化、和非托管代碼交互、對結構體進行序列化等場景下,了解字段的內存布局是非常重要的。
本文寫作時 最新的 .NET 正式版是 .NET 9,以后的版本不保證本文內容的準確性,僅供參考。
本文將介紹 .NET 中結構體字段的內存布局,包括字段的對齊(Alignment)、填充(Padding)以及如何使用 StructLayoutAttribute
來控制字段的內存布局。
對齊的目的是為了 CPU 訪問內存的效率,64 位系統和 32 位系統中對齊要求存在差異,下文如果沒有特別說明,均指 64 位系統。
填充則是為了滿足對齊要求而在字段之間或結構體末尾添加的額外字節。
結構體的對其規則同時適用于棧上和堆上的結構體結構體實例,方便起見,大部分例子將使用棧上結構體實例來演示。
一些資料是從 字段的偏移量(offset)為出發點來介紹字段的內存布局的,但筆者認為從字段的 內存地址 出發更容易理解。
由于一些資料并沒有找到明確的官方的解釋,筆者是在實驗和推導的基礎上總結出這些規則的,可能會有不準確的地方,歡迎讀者在評論區指出。
本文雖然沒有直接介紹引用類型的字段布局,但引用類型實例的字段的內存布局概念與結構體實例的內存布局是相同的。不同之處在于引用類型的默認布局是 LayoutKind.Auto
,而結構體的默認布局是 LayoutKind.Sequential
。讀者可以自己嘗試觀察引用類型實例字段的內存布局。
本文將使用下面的方法來觀察字段的內存地址:
void PrintPointerHeader()
{
Console.WriteLine(
$"| {"Expr",-15} | {"Address",-15} | {"Size",-4} | {"AlignedBySize",-13} | {"Addr/Size",-12} |");
}
unsafe void PrintPointerDetails<T>(
T* ptr,
[CallerArgumentExpression("ptr")] string? pointerExpr = null)
where T : unmanaged
{
ulong addressValue = (ulong)ptr;
ulong typeSize = (ulong)sizeof(T);
decimal addressDivBySize = addressValue / (decimal)typeSize;
bool isAlignedBySize = addressValue % typeSize == 0;
Console.WriteLine(
$"| {pointerExpr,-15} | {addressValue,-15} | {typeSize,-4} | {isAlignedBySize,-13} | {addressDivBySize,-12:0.##} |"
);
}
并使用 ObjectLayoutInspector 這個開源庫來觀察字段的內存布局。
項目地址:https://github.com/SergeyTeplyakov/ObjectLayoutInspector
nuget 包地址:https://www.nuget.org/packages/ObjectLayoutInspector
dotnet add package ObjectLayoutInspector --version 0.1.4
基本概念
以下是理解結構體字段布局的幾個關鍵點:
字段順序:字段在結構體實例中的排列順序,默認按聲明順序排列,但可以通過 StructLayoutAttribute
來控制。
對齊(Alignment):對齊需要分成三部分理解:
- 字段的對齊要求(alignment requirement):指字段在內存中的地址必須是其對齊要求的倍數。對于基元類型(primitive types),對齊要求默認等于其大小,非基元類型的對齊要求取決于結構體中最大字段的對齊要求。
- 結構體實例的大小:必須是結構體對齊要求的整數倍。
- 結構體實例的起始地址:在 64 位系統中,數據的地址按 8 字節 對齊有利于提升 CPU 的訪問效率,32 位系統中則為 4 字節對齊。
填充(Padding):為了滿足對齊要求,runtime 可能會在結構體實例字段之間及末尾插入填充字節。這些填充字節不會被顯式聲明,但會影響字段在內存中的實際布局。
結構體的默認字段布局
字段默認的對齊要求是類型的大小。例如,int
類型的字段需要在 4 字節對齊邊界(alignment boundary)上,而 double
類型的字段需要在 8 字節對齊邊界上。如果字段類型并非基元類型(primitive types),則對齊要求取決于結構體中最大字段的對齊要求。對齊要求為 2 的整數次冪,例如 1、2、4、8 等。最大對齊要求為 8 字節。
注意:decimal
不屬于基元類型,目前版本中由三個字段組成,實例大小為 16 字節,按 8 字節對齊。
Type layout for 'Decimal'
Size: 16 bytes. Paddings: 0 bytes (%0 of empty space)
|===============================|
| 0-3: Int32 _flags (4 bytes) |
|-------------------------------|
| 4-7: UInt32 _hi32 (4 bytes) |
|-------------------------------|
| 8-15: UInt64 _lo64 (8 bytes) |
|===============================|
下面是一個簡單的示例,展示了結構體字段的默認布局:
using System.Runtime.CompilerServices;
var foo = new Foo();
var bar = new Bar();
var baz = new Baz();
unsafe
{
PrintPointerHeader();
PrintPointerDetails(&foo);
PrintPointerDetails(&foo.a);
PrintPointerDetails(&foo.b);
PrintPointerDetails(&bar);
PrintPointerDetails(&bar.foo);
PrintPointerDetails(&bar.foo.a);
PrintPointerDetails(&bar.foo.b);
fixed (Foo* bazFooPtr = &baz.foo)
{
PrintPointerDetails(bazFooPtr);
PrintPointerDetails(&bazFooPtr->a);
PrintPointerDetails(&bazFooPtr->b);
}
}
struct Foo
{
public int a;
public long b;
}
struct Bar
{
public Foo foo;
}
class Baz
{
public Foo foo;
}
輸出結果如下:
| Expr | Address | Size | AlignedBySize | Addr/Size |
| &foo | 6095528264 | 16 | False | 380970516.5 |
| &foo.a | 6095528264 | 4 | True | 1523882066 |
| &foo.b | 6095528272 | 8 | True | 761941034 |
| &bar | 6095528248 | 16 | False | 380970515.5 |
| &bar.foo | 6095528248 | 16 | False | 380970515.5 |
| &bar.foo.a | 6095528248 | 4 | True | 1523882062 |
| &bar.foo.b | 6095528256 | 8 | True | 761941032 |
| bazFooPtr | 12885617264 | 16 | True | 805351079 |
| &bazFooPtr->a | 12885617264 | 4 | True | 3221404316 |
| &bazFooPtr->b | 12885617272 | 8 | True | 1610702159 |
首先看 Foo
結構體,它有兩個字段 a
和 b
,分別是 int
和 long
類型,對齊要求分別是 4 字節和 8 字節。
所以 Foo
實例在棧上的地址按照 8 字節 對齊(6095528264 / 8 = 761941033)。
a
字段是 foo
的第一個字段,它的地址也就是 foo
的起始地址,自然也滿足 int
的對齊要求(6095528264 / 4 = 1523882066)。
b
字段是 foo
的第二個字段,它的地址為 6095528272,滿足 long
的對齊要求(6095528272 / 8 = 761941032)。
Bar
結構體包含一個 Foo
類型的字段 foo
,它的對齊要求也是 8 字節(取最大字段 long 的對齊要求),所以 bar
的地址也是按照 8 字節對齊(6095528248 / 8 = 761941031)。bar.foo.a
和 bar.foo.b
的地址也滿足各自的對齊要求。
Baz
類包含一個 Foo
類型的字段 foo
,由于 Baz
是引用類型,所以它的實例在堆上分配內存。Baz
的 Foo
類型字段也依舊需要滿足 8 字節對齊要求(12885617264 / 8 = 1610702158)。
64 位系統與 32 位系統的對齊要求差異#
在 64 位系統中,結構體實例的起始地址默認按 8 字節對齊。
而在 32 位系統中,結構體實例的起始地址默認按 4 字節對齊。經筆者測試,CPU 為 intel 時 只按 4 字節對齊,CPU 為 AMD 時 如果結構體包含了 8 字節對齊的字段,則按 8 字節對齊,否則按 4 字節對齊。
首先在 64 位系統上運行下面的代碼:
using System.Runtime.CompilerServices;
using ObjectLayoutInspector;
unsafe
{
var foo = new Foo();
var bar = new Bar();
PrintPointerHeader();
PrintPointerDetails(&foo.a);
PrintPointerDetails(&foo.b);
PrintPointerDetails(&foo.c);
PrintPointerDetails(&bar.d);
PrintPointerDetails(&bar.e);
PrintPointerDetails(&bar.f);
}
TypeLayout.PrintLayout<Foo>();
TypeLayout.PrintLayout<Bar>();
struct Foo
{
public int a;
public long b;
public byte c;
}
struct Bar
{
public int d;
public int e;
public byte f;
}
輸出結果如下:
| Expr | Address | Size | AlignedBySize | Addr/Size |
| &foo.a | 985964996520 | 4 | True | 246491249130 |
| &foo.b | 985964996528 | 8 | True | 123245624566 |
| &foo.c | 985964996536 | 1 | True | 985964996536 |
| &bar.d | 985964996504 | 4 | True | 246491249126 |
| &bar.e | 985964996508 | 4 | True | 246491249127 |
| &bar.f | 985964996512 | 1 | True | 985964996512 |
Type layout for 'Foo'
Size: 24 bytes. Paddings: 11 bytes (%45 of empty space)
|==========================|
| 0-3: Int32 a (4 bytes) |
|--------------------------|
| 4-7: padding (4 bytes) |
|--------------------------|
| 8-15: Int64 b (8 bytes) |
|--------------------------|
| 16: Byte c (1 byte) |
|--------------------------|
| 17-23: padding (7 bytes) |
|==========================|
Type layout for 'Bar'
Size: 12 bytes. Paddings: 3 bytes (%25 of empty space)
|==========================|
| 0-3: Int32 d (4 bytes) |
|--------------------------|
| 4-7: Int32 e (4 bytes) |
|--------------------------|
| 8: Byte f (1 byte) |
|--------------------------|
| 9-11: padding (3 bytes) |
|==========================|
可以看到,Foo
和 Bar
結構體的實例大小分別為 24 字節和 12 字節,且它們的起始地址都滿足 8 字節對齊要求。
在 Windows 環境中,如果安裝了 x86 版本的 .NET SDK,可以在 csproj 文件中添加以下屬性來讓項目運行在 32 位的環境中:
<PropertyGroup>
<RuntimeIdentifier>win-x86</RuntimeIdentifier>
</PropertyGroup>
下面是 intel CPU 的輸出結果:
| Expr | Address | Size | AlignedBySize | Addr/Size |
| &foo.a | 43511772 | 4 | True | 10877943 |
| &foo.b | 43511780 | 8 | False | 5438972.5 |
| &foo.c | 43511788 | 1 | True | 43511788 |
| &bar.d | 43511760 | 4 | True | 10877940 |
| &bar.e | 43511764 | 4 | True | 10877941 |
| &bar.f | 43511768 | 1 | True | 43511768 |
Type layout for 'Foo'
Size: 24 bytes. Paddings: 11 bytes (%45 of empty space)
|==========================|
| 0-3: Int32 a (4 bytes) |
|--------------------------|
| 4-7: padding (4 bytes) |
|--------------------------|
| 8-15: Int64 b (8 bytes) |
|--------------------------|
| 16: Byte c (1 byte) |
|--------------------------|
| 17-23: padding (7 bytes) |
|==========================|
Type layout for 'Bar'
Size: 12 bytes. Paddings: 3 bytes (%25 of empty space)
|==========================|
| 0-3: Int32 d (4 bytes) |
|--------------------------|
| 4-7: Int32 e (4 bytes) |
|--------------------------|
| 8: Byte f (1 byte) |
|--------------------------|
| 9-11: padding (3 bytes) |
|==========================|
Foo
和 Bar
結構體的起始地址和字段地址都只滿足 4 字節對齊要求(43511772 / 4 = 10877943),而不是 8 字節對齊要求。
下面是 AMD CPU 的輸出結果:
運行上述代碼,輸出結果如下:
```bash
| Expr | Address | Size | AlignedBySize | Addr/Size |
| &foo.a | 47706560 | 4 | True | 11926640 |
| &foo.b | 47706568 | 8 | True | 5963321 |
| &foo.c | 47706576 | 1 | True | 47706576 |
| &bar.d | 47706548 | 4 | True | 11926637 |
| &bar.e | 47706552 | 4 | True | 11926638 |
| &bar.f | 47706556 | 1 | True | 47706556 |
Type layout for 'Foo'
Size: 24 bytes. Paddings: 11 bytes (%45 of empty space)
|==========================|
| 0-3: Int32 a (4 bytes) |
|--------------------------|
| 4-7: padding (4 bytes) |
|--------------------------|
| 8-15: Int64 b (8 bytes) |
|--------------------------|
| 16: Byte c (1 byte) |
|--------------------------|
| 17-23: padding (7 bytes) |
|==========================|
Type layout for 'Bar'
Size: 12 bytes. Paddings: 3 bytes (%25 of empty space)
|==========================|
| 0-3: Int32 d (4 bytes) |
|--------------------------|
| 4-7: Int32 e (4 bytes) |
|--------------------------|
| 8: Byte f (1 byte) |
|--------------------------|
| 9-11: padding (3 bytes) |
|==========================|
Foo
的起始地址仍然滿足 8 字節對齊要求,但 Bar
的起始地址不再滿足 8 字節對齊要求(47706548 / 8 = 5963318.5),而是滿足 4 字節對齊要求(47706548 / 4 = 11926637)。
默認字段布局中 對齊要求 與 偏移量 的關系#
偏移量(offset)是指字段相對于結構體實例起始地址的距離,決定了字段在內存中的位置。
偏移量的值取決于對齊要求和字段的順序,會在未被順序在前的字段占用的內存空間中取對齊要求的最小整數倍。
下面幾個設計確保了不管結構體實例的起始地址如何,任意一個字段只要給定一個滿足對齊要求的偏移量,就可以滿足該字段的對齊要求:
- 對齊要求總是 2 的整數次冪。
- 實例的起始地址(按 8 字節 對齊)總是滿足最大字段的對齊要求。
- 偏移量的值是對齊要求的整數倍
下面做一個簡單的推導來幫助讀者理解:
假設結構體中最大字段的對齊要求為 2^m(m 為 <= 8 的非負整數),則 runtime 會保證結構體實例的起始地址也是 2^m 的整數倍,可記作 2^m * k(k為非負整數)。
若某字段的對齊要求為 2^n(n≤m),其偏移量必為 2^n 的整數倍,記為 2^n * f(f為非負整數)。
則該字段實際地址為:
結構體起始地址 + 字段偏移量 = (2^m * k) + (2^n * f)
由于 2^m 必定可以被 2^n 整除(因為 n≤m),所以無論 k 和 f 取何值,上述字段地址總能被 2^n 整除。這就保證了該字段的地址總是滿足其對齊要求。
因此,只要給每個字段的 偏移量 選擇其 對齊要求 的整數倍,就能保證結構體任何實例、任意字段的地址都天然對齊,而無需依賴結構體起始地址的額外信息。
unsafe
{
var foo = new Foo();
var addr = (ulong)&foo;
Console.WriteLine($"a offset: {(ulong)&foo.a - addr}");
Console.WriteLine($"b offset: {(ulong)&foo.b - addr}");
Console.WriteLine($"c offset: {(ulong)&foo.c - addr}");
}
struct Foo
{
public int a;
public long b;
public byte c;
}
輸出結果如下:
a offset: 0
b offset: 8
c offset: 16
填充(Padding)分為兩部分:
字段之間的填充:為了滿足對齊要求,.NET 可能會在字段之間插入填充字節。字段之間的填充由字段的偏移量決定。
結構體末尾的填充:為了確保結構體的大小是最大字段對齊要求的倍數,.NET 可能會在結構體末尾添加填充字節。末尾填充保證了數組中連續的結構體實例在內存中也滿足對齊要求。
借助 ObjectLayoutInspector
庫,我們可以觀察到結構體的內存布局,包括字段之間的填充和結構體末尾的填充。
using ObjectLayoutInspector;
TypeLayout.PrintLayout<Foo>();
TypeLayout.PrintLayout<Bar>();
struct Foo
{
public int a;
public long b;
public byte c;
}
struct Bar
{
public byte c;
public int a;
public long b;
}
輸出結果如下:
Type layout for 'Foo'
Size: 24 bytes. Paddings: 11 bytes (%45 of empty space)
|==========================|
| 0-3: Int32 a (4 bytes) |
|--------------------------|
| 4-7: padding (4 bytes) |
|--------------------------|
| 8-15: Int64 b (8 bytes) |
|--------------------------|
| 16: Byte c (1 byte) |
|--------------------------|
| 17-23: padding (7 bytes) |
|==========================|
Type layout for 'Bar'
Size: 16 bytes. Paddings: 3 bytes (%18 of empty space)
|==========================|
| 0: Byte c (1 byte) |
|--------------------------|
| 1-3: padding (3 bytes) |
|--------------------------|
| 4-7: Int32 a (4 bytes) |
|--------------------------|
| 8-15: Int64 b (8 bytes) |
|==========================|
Foo
和 Bar
雖然包含了相同類型的字段,但由于字段的順序不同,導致它們的內存布局和填充字節數量也不同。Foo
需要在在末尾添加 7 字節的填充才能滿足其大小是最大字段對齊要求的倍數。
包含引用類型字段的結構體的默認字段布局
如果結構體包含引用類型字段,則該結構體的默認布局為 LayoutKind.Auto
。
using ObjectLayoutInspector;
TypeLayout.PrintLayout<Foo>();
struct Foo
{
public int a;
public string b;
public byte c;
}
Type layout for 'Foo'
Size: 16 bytes. Paddings: 3 bytes (%18 of empty space)
|===========================|
| 0-7: String b (8 bytes) |
|---------------------------|
| 8-11: Int32 a (4 bytes) |
|---------------------------|
| 12: Byte c (1 byte) |
|---------------------------|
| 13-15: padding (3 bytes) |
|===========================|
用 StructLayoutAttribute
控制字段布局
在某些情況下,我們可能需要控制結構體字段的內存布局,以滿足特定的性能要求或與非托管代碼交互。可以使用 StructLayoutAttribute
特性來控制結構體的內存布局。
StructLayoutAttribute
有兩個重要的屬性:
LayoutKind
:指定結構體的布局方式,可以是 Sequential
(按聲明順序排列)、Explicit
(顯式指定字段偏移量)或 Auto
(自動布局)。
Pack
:指定結構體及其字段的對齊要求,其值必須為 0、1、2、4、8、16、32、64 或 128,否則無法編譯成功,默認值為 0。** 指定 Pack
> 8 時, 等效于 Pack = 8
,因為目前版本沒有任何類型的對齊要求超過 8 字節。**
Pack
屬性在 LayoutKind.Auto
布局中無效。在 LayoutKind.Sequential
布局中,Pack
屬性用于指定字段的對齊要求及結構體實例的對齊要求;在 LayoutKind.Explicit
布局中,Pack
屬性用于結構體的對齊要求,會影響結構體實例的末尾填充。
LayoutKind.Sequential#
Pack 為 0 時等于默認布局#
using System.Runtime.InteropServices;
using ObjectLayoutInspector;
TypeLayout.PrintLayout<Foo>();
[StructLayout(LayoutKind.Sequential, Pack = 0)]
struct Foo
{
public int a;
public long b;
public byte c;
}
Type layout for 'Foo'
Size: 24 bytes. Paddings: 11 bytes (%45 of empty space)
|==========================|
| 0-3: Int32 a (4 bytes) |
|--------------------------|
| 4-7: padding (4 bytes) |
|--------------------------|
| 8-15: Int64 b (8 bytes) |
|--------------------------|
| 16: Byte c (1 byte) |
|--------------------------|
| 17-23: padding (7 bytes) |
|==========================|
Pack 不為 0 時,取 Pack 和 字段類型大小 的較小值#
Pack
設置為 4 時,int
和 long
字段的對齊要求都將被設置為 4 字節,而 byte
字段的對齊要求仍然是 1 字節。結構體的對齊要求是 4 字節。
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using ObjectLayoutInspector;
{
var foo = new Foo();
PrintPointerHeader();
PrintPointerDetails(&foo.a);
PrintPointerDetails(&foo.b);
PrintPointerDetails(&foo.c);
}
TypeLayout.PrintLayout<Foo>();
[StructLayout(LayoutKind.Sequential, Pack = 4)]
struct Foo
{
public int a;
public long b;
public byte c;
}
| Expr | Address | Size | AlignedBySize | Addr/Size |
| &foo.a | 782597876240 | 4 | True | 195649469060 |
| &foo.b | 782597876244 | 8 | False | 97824734530.5 |
| &foo.c | 782597876252 | 1 | True | 782597876252 |
Type layout for 'Foo'
Size: 16 bytes. Paddings: 3 bytes (%18 of empty space)
|==========================|
| 0-3: Int32 a (4 bytes) |
|--------------------------|
| 4-11: Int64 b (8 bytes) |
|--------------------------|
| 12: Byte c (1 byte) |
|--------------------------|
| 13-15: padding (3 bytes) |
|==========================|
結構體實例的起始地址按 8 字節 對齊(782597876240 / 8 = 97824734530)。但其大小取滿足 4 字節對齊要求的最小整數倍 16 字節( 末尾字段 c 的偏移量為 12,最小只能取到 16),并在末尾添加 3 字節的填充。
Pack 設置為 1 時,會形成密集的字段布局#
當 Pack
設置為 1 時,所有字段的對齊要求都將被設置為 1 字節,這意味著結構體實例將按照 1 字節對齊。此時,結構體實例的字段將緊密排列,不會有額外的填充字節。
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using ObjectLayoutInspector;
var foo = new Foo
{
a = 1,
b = 2,
c = 3
};
unsafe
{
PrintPointerHeader();
PrintPointerDetails(&foo.a);
PrintPointerDetails(&foo.b);
PrintPointerDetails(&foo.c);
}
TypeLayout.PrintLayout<Foo>();
[StructLayout(LayoutKind.Sequential, Pack = 1)]
struct Foo
{
public int a;
public long b;
public byte c;
}
| Expr | Address | Size | AlignedBySize | Addr/Size |
| &foo.a | 302463314288 | 4 | True | 75615828572 |
| &foo.b | 302463314292 | 8 | False | 37807914286.5 |
| &foo.c | 302463314300 | 1 | True | 302463314300 |
Type layout for 'Foo'
Size: 13 bytes. Paddings: 0 bytes (%0 of empty space)
|==========================|
| 0-3: Int32 a (4 bytes) |
|--------------------------|
| 4-11: Int64 b (8 bytes) |
|--------------------------|
| 12: Byte c (1 byte) |
|==========================|
起始地址為 8 的倍數(302463314288 / 8 = 37807914286),但結構體實例的大小變為 13 字節,沒有末尾填充。
Pack 不為 0 的結構體作為其他結構體字段時#
如果外層的結構體采用默認字段布局則,則其實例的起始地址取決嵌套結構體的最大字段默認對齊要求,其實例大小取決該結構體的最大字段對齊要求。
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using ObjectLayoutInspector;
unsafe
{
PrintPointerHeader();
PrintPointerDetails(&bar.foo.a);
PrintPointerDetails(&bar.foo.b);
PrintPointerDetails(&bar.foo.c);
PrintPointerDetails(&bar.d);
}
TypeLayout.PrintLayout<Bar>();
[StructLayout(LayoutKind.Sequential, Pack = 1)]
struct Foo
{
public int a;
public long b;
public byte c;
}
struct Bar
{
public Foo foo;
public int d;
}
| Expr | Address | Size | AlignedBySize | Addr/Size |
| &bar.foo.a | 724703897336 | 4 | True | 181175974334 |
| &bar.foo.b | 724703897340 | 8 | False | 90587987167.5 |
| &bar.foo.c | 724703897348 | 1 | True | 724703897348 |
| &bar.d | 724703897352 | 4 | True | 181175974338 |
Type layout for 'Bar'
Size: 20 bytes. Paddings: 3 bytes (%15 of empty space)
|==============================|
| 0-12: Foo foo (13 bytes) |
| |==========================| |
| | 0-3: Int32 a (4 bytes) | |
| |--------------------------| |
| | 4-11: Int64 b (8 bytes) | |
| |--------------------------| |
| | 12: Byte c (1 byte) | |
| |==========================| |
|------------------------------|
| 13-15: padding (3 bytes) |
|------------------------------|
| 16-19: Int32 d (4 bytes) |
|==============================|
在上面的例子中,Bar
結構體包含一個 Foo
類型的字段 foo
。
Bar
的實例起始地址也滿足 8 字節對齊要求(724703897336 / 8 = 90587987167)。
foo
的對齊要求為 1 字節, d
的對齊要求為 4 字節,所以 Bar
的實例大小為 20 字節(4 的整數倍),并在foo
和 d
之間添加了 3 字節的填充。
LayoutKind.Explicit#
Pack 為 0 時,結構體按照最大字段默認對齊要求對齊#
在 Explicit
布局中,我們需要顯式指定每個字段的偏移量。使用 FieldOffsetAttribute
來指定字段的偏移量。此時偏移量可以是任意值,甚至允許重疊字段。
此時雖然字段地址可能由于是任意值而不滿足對齊要求,但結構體實例的起始地址依舊按 8 字節 對齊,且結構體實例的大小是最大字段對齊要求的整數倍。
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using ObjectLayoutInspector;
var foo = new Foo
{
a = 1,
b = 2,
c = 3
};
unsafe
{
PrintPointerHeader();
PrintPointerDetails(&foo.a);
PrintPointerDetails(&foo.b);
PrintPointerDetails(&foo.c);
}
TypeLayout.PrintLayout<Foo>();
[StructLayout(LayoutKind.Explicit, Pack = 0)]
struct Foo
{
[FieldOffset(0)]
public int a;
[FieldOffset(3)]
public long b;
[FieldOffset(11)]
public byte c;
}
| Expr | Address | Size | AlignedBySize | Addr/Size |
| &foo.a | 6095151432 | 4 | True | 1523787858 |
| &foo.b | 6095151435 | 8 | False | 761893929.38 |
| &foo.c | 6095151443 | 1 | True | 6095151443 |
Type layout for 'Foo'
Size: 16 bytes. Paddings: 4 bytes (%25 of empty space)
|==========================|
| 0-3: Int32 a (4 bytes) |
|--------------------------|
| 3-10: Int64 b (8 bytes) |
|--------------------------|
| 11: Byte c (1 byte) |
|--------------------------|
| 12-15: padding (4 bytes) |
|==========================|
上面例子中,Foo
結構體的字段 a
、b
和 c
的偏移量分別為 0、3 和 11。可以看到,雖然字段的地址不再滿足對齊要求,但結構體實例的起始地址仍然按 8 字節 對齊(6095151432 / 8 = 761893929),且結構體實例的大小為 16 字節(最大字段對齊要求的整數倍),末尾添加了 4 字節的填充。
如果將 c
字段的偏移量改為 16,則結構體實例的大小將變為 24 字節,并且會在末尾添加 7 字節的填充。
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using ObjectLayoutInspector;
var foo = new Foo
{
a = 1,
b = 2,
c = 3
};
unsafe
{
PrintPointerHeader();
PrintPointerDetails(&foo.a);
PrintPointerDetails(&foo.b);
PrintPointerDetails(&foo.c);
}
TypeLayout.PrintLayout<Foo>();
| Expr | Address | Size | AlignedBySize | Addr/Size |
| &foo.a | 6166536512 | 4 | True | 1541634128 |
| &foo.b | 6166536515 | 8 | False | 770817064.38 |
| &foo.c | 6166536528 | 1 | True | 6166536528 |
Type layout for 'Foo'
Size: 24 bytes. Paddings: 12 bytes (%50 of empty space)
|==========================|
| 0-3: Int32 a (4 bytes) |
|--------------------------|
| 3-10: Int64 b (8 bytes) |
|--------------------------|
| 11-15: padding (5 bytes) |
|--------------------------|
| 16: Byte c (1 byte) |
|--------------------------|
| 17-23: padding (7 bytes) |
|==========================|
Pack 不為 0 時,結構體實例按照 Pack 與 最大字段對齊要求 的較小值對齊#
在 Explicit
布局中,如果設置了 Pack
屬性且不為 0,則結構體實例將按照 Pack
的值對齊。字段的偏移量仍然可以是任意值,但結構體實例的大小將受到 Pack
屬性的影響。
var foo = new Foo
{
a = 1,
b = 2,
c = 3
};
unsafe
{
PrintPointerHeader();
PrintPointerDetails(&foo.a);
PrintPointerDetails(&foo.b);
PrintPointerDetails(&foo.c);
}
TypeLayout.PrintLayout<Foo>();
[StructLayout(LayoutKind.Explicit, Pack = 4)]
struct Foo
{
[FieldOffset(0)]
public int a;
[FieldOffset(5)]
public long b;
[FieldOffset(16)]
public byte c;
}
| Expr | Address | Size | AlignedBySize | Addr/Size |
| &foo.a | 6122676544 | 4 | True | 1530669136 |
| &foo.b | 6122676549 | 8 | False | 765334568.63 |
| &foo.c | 6122676560 | 1 | True | 6122676560 |
Type layout for 'Foo'
Size: 20 bytes. Paddings: 7 bytes (%35 of empty space)
|==========================|
| 0-3: Int32 a (4 bytes) |
|--------------------------|
| 4: padding (1 byte) |
|--------------------------|
| 5-12: Int64 b (8 bytes) |
|--------------------------|
| 13-15: padding (3 bytes) |
|--------------------------|
| 16: Byte c (1 byte) |
|--------------------------|
| 17-19: padding (3 bytes) |
|==========================|
在上面的例子中,由于 Pack
屬性設置為 4,與 long 類型的 8 字節 比較則結構體實例應按照 4 字節 對齊。因為 c
的偏移量為 16,所以 Foo
的大小在取值此時符合條件的 4 的最小整數倍后變為 20 字節,并在末尾添加了 3 字節 的填充。
改成 Pack = 128
后,結構體實例的大小按照最大字段默認對齊要求 8 字節 對齊。
var foo = new Foo
{
a = 1,
b = 2,
c = 3
};
unsafe
{
PrintPointerHeader();
PrintPointerDetails(&foo.a);
PrintPointerDetails(&foo.b);
PrintPointerDetails(&foo.c);
}
TypeLayout.PrintLayout<Foo>();
[StructLayout(LayoutKind.Explicit, Pack = 128)]
struct Foo
{
[FieldOffset(0)]
public int a;
[FieldOffset(5)]
public long b;
[FieldOffset(16)]
public byte c;
}
| Expr | Address | Size | AlignedBySize | Addr/Size |
| &foo.a | 6104211776 | 4 | True | 1526052944 |
| &foo.b | 6104211781 | 8 | False | 763026472.63 |
| &foo.c | 6104211792 | 1 | True | 6104211792 |
Type layout for 'Foo'
Size: 24 bytes. Paddings: 11 bytes (%45 of empty space)
|==========================|
| 0-3: Int32 a (4 bytes) |
|--------------------------|
| 4: padding (1 byte) |
|--------------------------|
| 5-12: Int64 b (8 bytes) |
|--------------------------|
| 13-15: padding (3 bytes) |
|--------------------------|
| 16: Byte c (1 byte) |
|--------------------------|
| 17-23: padding (7 bytes) |
|==========================|
將 Pack 屬性設置為 1 可以消除結構體實例的末尾填充#
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using ObjectLayoutInspector;
var foo = new Foo
{
a = 1,
b = 2,
c = 3
};
unsafe
{
PrintPointerHeader();
PrintPointerDetails(&foo.a);
PrintPointerDetails(&foo.b);
PrintPointerDetails(&foo.c);
}
TypeLayout.PrintLayout<Foo>();
[StructLayout(LayoutKind.Explicit, Pack = 1)]
struct Foo
{
[FieldOffset(0)]
public int a;
[FieldOffset(5)]
public long b;
[FieldOffset(16)]
public byte c;
}
| Expr | Address | Size | AlignedBySize | Addr/Size |
| &foo.a | 468685679112 | 4 | True | 117171419778 |
| &foo.b | 468685679117 | 8 | False | 58585709889.63 |
| &foo.c | 468685679128 | 1 | True | 468685679128 |
Type layout for 'Foo'
Size: 17 bytes. Paddings: 4 bytes (%23 of empty space)
|==========================|
| 0-3: Int32 a (4 bytes) |
|--------------------------|
| 4: padding (1 byte) |
|--------------------------|
| 5-12: Int64 b (8 bytes) |
|--------------------------|
| 13-15: padding (3 bytes) |
|--------------------------|
| 16: Byte c (1 byte) |
|==========================|
此時實例的起始地址仍然按 8 字節 對齊(468685679112 / 8 = 58585709889),但實例的大小則是 17 字節,末尾填充為 0 字節。
Pack 屬性不為 0 的結構體作為其他結構體字段時#
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using ObjectLayoutInspector;
var bar = new Bar
{
foo = new Foo(),
d = 4
};
unsafe
{
PrintPointerHeader();
PrintPointerDetails(&bar.foo.a);
PrintPointerDetails(&bar.foo.b);
PrintPointerDetails(&bar.foo.c);
PrintPointerDetails(&bar.d);
}
TypeLayout.PrintLayout<Bar>();
[StructLayout(LayoutKind.Explicit, Pack = 1)]
struct Foo
{
[FieldOffset(0)]
public int a;
[FieldOffset(5)]
public long b;
[FieldOffset(16)]
public byte c;
}
struct Bar
{
public Foo foo;
public int d;
}
| Expr | Address | Size | AlignedBySize | Addr/Size |
| &bar.foo.a | 967090628200 | 4 | True | 241772657050 |
| &bar.foo.b | 967090628205 | 8 | False | 120886328525.63 |
| &bar.foo.c | 967090628216 | 1 | True | 967090628216 |
| &bar.d | 967090628220 | 4 | True | 241772657055 |
Type layout for 'Bar'
Size: 24 bytes. Paddings: 7 bytes (%29 of empty space)
|==============================|
| 0-16: Foo foo (17 bytes) |
| |==========================| |
| | 0-3: Int32 a (4 bytes) | |
| |--------------------------| |
| | 4: padding (1 byte) | |
| |--------------------------| |
| | 5-12: Int64 b (8 bytes) | |
| |--------------------------| |
| | 13-15: padding (3 bytes) | |
| |--------------------------| |
| | 16: Byte c (1 byte) | |
| |==========================| |
|------------------------------|
| 17-19: padding (3 bytes) |
|------------------------------|
| 20-23: Int32 d (4 bytes) |
|==============================|
在上面的例子中,Bar
結構體包含一個 Foo
類型的字段 foo
,由于 Foo
的最大字段對齊要求為 8 字節,所以 Bar
的實例起始地址也滿足 8 字節對齊要求(967090628200 / 8 = 120886328525)。
foo
的對齊要求為 1 字節, d
的對齊要求為 4 字節,所以 Bar
的實例大小為 24 字節(4 的整數倍),并在foo
和 d
之間添加了 3 字節的填充。
LayoutKind.Auto#
使用 LayoutKind.Auto
時,runtime 將根據字段的類型和聲明順序自動確定字段的布局,會調整實例字段的排列順序和對齊要求,以優化內存布局和性能。
LayoutKind.Auto
也是引用類型實例字段的默認布局方式。
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using ObjectLayoutInspector;
var foo = new Foo
{
a = 1,
b = 2,
c = 3
};
unsafe
{
PrintPointerHeader();
PrintPointerDetails(&foo.a);
PrintPointerDetails(&foo.b);
PrintPointerDetails(&foo.c);
}
TypeLayout.PrintLayout<Foo>();
[StructLayout(LayoutKind.Auto)]
struct Foo
{
public int a;
public long b;
public byte c;
}
| Expr | Address | Size | AlignedBySize | Addr/Size |
| &foo.a | 6166815056 | 4 | True | 1541703764 |
| &foo.b | 6166815048 | 8 | True | 770851881 |
| &foo.c | 6166815060 | 1 | True | 6166815060 |
Type layout for 'Foo'
Size: 16 bytes. Paddings: 3 bytes (%18 of empty space)
|==========================|
| 0-7: Int64 b (8 bytes) |
|--------------------------|
| 8-11: Int32 a (4 bytes) |
|--------------------------|
| 12: Byte c (1 byte) |
|--------------------------|
| 13-15: padding (3 bytes) |
|==========================|
上面例子中,Foo
結構體的字段 b
被放在了前面,各字段都按照其類型大小進行了對齊,相較于默認布局,Foo
結構體的內存布局更加緊湊,減少了填充字節的數量。
等效于于下面的結構體定義
[StructLayout(LayoutKind.Sequential, Pack = 0)]
struct Foo
{
public long b;
public int a;
public byte c;
}
作為數組元素時的結構體實例
默認字段布局#
默認布局的結構體實例在數組中也會按照最大字段對齊要求進行對齊。每個結構體實例的起始地址都是該結構體最大字段對齊要求的整數倍。
因此默認布局下,數組中的每個結構體的字段都是滿足對齊要求的。
using System.Runtime.CompilerServices;
unsafe
{
var arr = stackalloc Foo[] { new Foo(), new Foo() };
PrintPointerHeader();
PrintPointerDetails(&arr[0].a);
PrintPointerDetails(&arr[0].b);
PrintPointerDetails(&arr[0].c);
PrintPointerDetails(&arr[1].a);
PrintPointerDetails(&arr[1].b);
PrintPointerDetails(&arr[1].c);
}
struct Foo
{
public int a;
public long b;
public byte c;
}
| Expr | Address | Size | AlignedBySize | Addr/Size |
| &arr[0].a | 1029625933216 | 4 | True | 257406483304 |
| &arr[0].b | 1029625933224 | 8 | True | 128703241653 |
| &arr[0].c | 1029625933232 | 1 | True | 1029625933232 |
| &arr[1].a | 1029625933240 | 4 | True | 257406483310 |
| &arr[1].b | 1029625933248 | 8 | True | 128703241656 |
| &arr[1].c | 1029625933256 | 1 | True | 1029625933256 |
非默認字段布局#
因為數組中結構體實例是連續存儲的,如果結構體實例的字段布局進行了非默認的調整,則可能導致第二個開始的構體實例完全不滿足對齊要求(包括實例的起始地址和字段地址)。
using System.Runtime.CompilerServices;
unsafe
{
var arr = stackalloc Foo[] { new Foo(), new Foo() };
PrintPointerHeader();
PrintPointerDetails(&arr[0].a);
PrintPointerDetails(&arr[0].b);
PrintPointerDetails(&arr[0].c);
PrintPointerDetails(&arr[1].a);
PrintPointerDetails(&arr[1].b);
PrintPointerDetails(&arr[1].c);
}
[StructLayout(LayoutKind.Sequential, Pack = 1)]
struct Foo
{
public int a;
public long b;
public byte c;
}
| Expr | Address | Size | AlignedBySize | Addr/Size |
| &arr[0].a | 654696769936 | 4 | True | 163674192484 |
| &arr[0].b | 654696769940 | 8 | False | 81837096242.5 |
| &arr[0].c | 654696769948 | 1 | True | 654696769948 |
| &arr[1].a | 654696769949 | 4 | False | 163674192487.25 |
| &arr[1].b | 654696769953 | 8 | False | 81837096244.13 |
| &arr[1].c | 654696769961 | 1 | True | 654696769961 |