前言
大部分情況下我們并不需要關(guān)心結(jié)構(gòu)體字段的內(nèi)存布局,但是在一些特殊情況下,比如性能優(yōu)化、和非托管代碼交互、對(duì)結(jié)構(gòu)體進(jìn)行序列化等場(chǎng)景下,了解字段的內(nèi)存布局是非常重要的。
本文寫作時(shí) 最新的 .NET 正式版是 .NET 9,以后的版本不保證本文內(nèi)容的準(zhǔn)確性,僅供參考。
本文將介紹 .NET 中結(jié)構(gòu)體字段的內(nèi)存布局,包括字段的對(duì)齊(Alignment)、填充(Padding)以及如何使用 StructLayoutAttribute
來(lái)控制字段的內(nèi)存布局。
對(duì)齊的目的是為了 CPU 訪問(wèn)內(nèi)存的效率,64 位系統(tǒng)和 32 位系統(tǒng)中對(duì)齊要求存在差異,下文如果沒(méi)有特別說(shuō)明,均指 64 位系統(tǒng)。
填充則是為了滿足對(duì)齊要求而在字段之間或結(jié)構(gòu)體末尾添加的額外字節(jié)。
結(jié)構(gòu)體的對(duì)其規(guī)則同時(shí)適用于棧上和堆上的結(jié)構(gòu)體結(jié)構(gòu)體實(shí)例,方便起見(jiàn),大部分例子將使用棧上結(jié)構(gòu)體實(shí)例來(lái)演示。
一些資料是從 字段的偏移量(offset)為出發(fā)點(diǎn)來(lái)介紹字段的內(nèi)存布局的,但筆者認(rèn)為從字段的 內(nèi)存地址 出發(fā)更容易理解。
由于一些資料并沒(méi)有找到明確的官方的解釋,筆者是在實(shí)驗(yàn)和推導(dǎo)的基礎(chǔ)上總結(jié)出這些規(guī)則的,可能會(huì)有不準(zhǔn)確的地方,歡迎讀者在評(píng)論區(qū)指出。
本文雖然沒(méi)有直接介紹引用類型的字段布局,但引用類型實(shí)例的字段的內(nèi)存布局概念與結(jié)構(gòu)體實(shí)例的內(nèi)存布局是相同的。不同之處在于引用類型的默認(rèn)布局是 LayoutKind.Auto
,而結(jié)構(gòu)體的默認(rèn)布局是 LayoutKind.Sequential
。讀者可以自己嘗試觀察引用類型實(shí)例字段的內(nèi)存布局。
本文將使用下面的方法來(lái)觀察字段的內(nèi)存地址:
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 這個(gè)開源庫(kù)來(lái)觀察字段的內(nèi)存布局。
項(xiàng)目地址:https://github.com/SergeyTeplyakov/ObjectLayoutInspector
nuget 包地址:https://www.nuget.org/packages/ObjectLayoutInspector
dotnet add package ObjectLayoutInspector --version 0.1.4
基本概念
以下是理解結(jié)構(gòu)體字段布局的幾個(gè)關(guān)鍵點(diǎn):
字段順序:字段在結(jié)構(gòu)體實(shí)例中的排列順序,默認(rèn)按聲明順序排列,但可以通過(guò) StructLayoutAttribute
來(lái)控制。
對(duì)齊(Alignment):對(duì)齊需要分成三部分理解:
- 字段的對(duì)齊要求(alignment requirement):指字段在內(nèi)存中的地址必須是其對(duì)齊要求的倍數(shù)。對(duì)于基元類型(primitive types),對(duì)齊要求默認(rèn)等于其大小,非基元類型的對(duì)齊要求取決于結(jié)構(gòu)體中最大字段的對(duì)齊要求。
- 結(jié)構(gòu)體實(shí)例的大?。罕仨毷墙Y(jié)構(gòu)體對(duì)齊要求的整數(shù)倍。
- 結(jié)構(gòu)體實(shí)例的起始地址:在 64 位系統(tǒng)中,數(shù)據(jù)的地址按 8 字節(jié) 對(duì)齊有利于提升 CPU 的訪問(wèn)效率,32 位系統(tǒng)中則為 4 字節(jié)對(duì)齊。
填充(Padding):為了滿足對(duì)齊要求,runtime 可能會(huì)在結(jié)構(gòu)體實(shí)例字段之間及末尾插入填充字節(jié)。這些填充字節(jié)不會(huì)被顯式聲明,但會(huì)影響字段在內(nèi)存中的實(shí)際布局。
結(jié)構(gòu)體的默認(rèn)字段布局
對(duì)齊#
字段默認(rèn)的對(duì)齊要求是類型的大小。例如,int
類型的字段需要在 4 字節(jié)對(duì)齊邊界(alignment boundary)上,而 double
類型的字段需要在 8 字節(jié)對(duì)齊邊界上。如果字段類型并非基元類型(primitive types),則對(duì)齊要求取決于結(jié)構(gòu)體中最大字段的對(duì)齊要求。對(duì)齊要求為 2 的整數(shù)次冪,例如 1、2、4、8 等。最大對(duì)齊要求為 8 字節(jié)。
注意:decimal
不屬于基元類型,目前版本中由三個(gè)字段組成,實(shí)例大小為 16 字節(jié),按 8 字節(jié)對(duì)齊。
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) |
|===============================|
下面是一個(gè)簡(jiǎn)單的示例,展示了結(jié)構(gòu)體字段的默認(rèn)布局:
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;
}
輸出結(jié)果如下:
| 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
結(jié)構(gòu)體,它有兩個(gè)字段 a
和 b
,分別是 int
和 long
類型,對(duì)齊要求分別是 4 字節(jié)和 8 字節(jié)。
所以 Foo
實(shí)例在棧上的地址按照 8 字節(jié) 對(duì)齊(6095528264 / 8 = 761941033)。
a
字段是 foo
的第一個(gè)字段,它的地址也就是 foo
的起始地址,自然也滿足 int
的對(duì)齊要求(6095528264 / 4 = 1523882066)。
b
字段是 foo
的第二個(gè)字段,它的地址為 6095528272,滿足 long
的對(duì)齊要求(6095528272 / 8 = 761941032)。
Bar
結(jié)構(gòu)體包含一個(gè) Foo
類型的字段 foo
,它的對(duì)齊要求也是 8 字節(jié)(取最大字段 long 的對(duì)齊要求),所以 bar
的地址也是按照 8 字節(jié)對(duì)齊(6095528248 / 8 = 761941031)。bar.foo.a
和 bar.foo.b
的地址也滿足各自的對(duì)齊要求。
Baz
類包含一個(gè) Foo
類型的字段 foo
,由于 Baz
是引用類型,所以它的實(shí)例在堆上分配內(nèi)存。Baz
的 Foo
類型字段也依舊需要滿足 8 字節(jié)對(duì)齊要求(12885617264 / 8 = 1610702158)。
64 位系統(tǒng)與 32 位系統(tǒng)的對(duì)齊要求差異#
在 64 位系統(tǒng)中,結(jié)構(gòu)體實(shí)例的起始地址默認(rèn)按 8 字節(jié)對(duì)齊。
而在 32 位系統(tǒng)中,結(jié)構(gòu)體實(shí)例的起始地址默認(rèn)按 4 字節(jié)對(duì)齊。經(jīng)筆者測(cè)試,CPU 為 intel 時(shí) 只按 4 字節(jié)對(duì)齊,CPU 為 AMD 時(shí) 如果結(jié)構(gòu)體包含了 8 字節(jié)對(duì)齊的字段,則按 8 字節(jié)對(duì)齊,否則按 4 字節(jié)對(duì)齊。
首先在 64 位系統(tǒng)上運(yùn)行下面的代碼:
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;
}
輸出結(jié)果如下:
| 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
結(jié)構(gòu)體的實(shí)例大小分別為 24 字節(jié)和 12 字節(jié),且它們的起始地址都滿足 8 字節(jié)對(duì)齊要求。
在 Windows 環(huán)境中,如果安裝了 x86 版本的 .NET SDK,可以在 csproj 文件中添加以下屬性來(lái)讓項(xiàng)目運(yùn)行在 32 位的環(huán)境中:
<PropertyGroup>
<RuntimeIdentifier>win-x86</RuntimeIdentifier>
</PropertyGroup>
下面是 intel CPU 的輸出結(jié)果:
| 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
結(jié)構(gòu)體的起始地址和字段地址都只滿足 4 字節(jié)對(duì)齊要求(43511772 / 4 = 10877943),而不是 8 字節(jié)對(duì)齊要求。
下面是 AMD CPU 的輸出結(jié)果:
運(yùn)行上述代碼,輸出結(jié)果如下:
```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 字節(jié)對(duì)齊要求,但 Bar
的起始地址不再滿足 8 字節(jié)對(duì)齊要求(47706548 / 8 = 5963318.5),而是滿足 4 字節(jié)對(duì)齊要求(47706548 / 4 = 11926637)。
默認(rèn)字段布局中 對(duì)齊要求 與 偏移量 的關(guān)系#
偏移量(offset)是指字段相對(duì)于結(jié)構(gòu)體實(shí)例起始地址的距離,決定了字段在內(nèi)存中的位置。
偏移量的值取決于對(duì)齊要求和字段的順序,會(huì)在未被順序在前的字段占用的內(nèi)存空間中取對(duì)齊要求的最小整數(shù)倍。
下面幾個(gè)設(shè)計(jì)確保了不管結(jié)構(gòu)體實(shí)例的起始地址如何,任意一個(gè)字段只要給定一個(gè)滿足對(duì)齊要求的偏移量,就可以滿足該字段的對(duì)齊要求:
- 對(duì)齊要求總是 2 的整數(shù)次冪。
- 實(shí)例的起始地址(按 8 字節(jié) 對(duì)齊)總是滿足最大字段的對(duì)齊要求。
- 偏移量的值是對(duì)齊要求的整數(shù)倍
下面做一個(gè)簡(jiǎn)單的推導(dǎo)來(lái)幫助讀者理解:
假設(shè)結(jié)構(gòu)體中最大字段的對(duì)齊要求為 2^m(m 為 <= 8 的非負(fù)整數(shù)),則 runtime 會(huì)保證結(jié)構(gòu)體實(shí)例的起始地址也是 2^m 的整數(shù)倍,可記作 2^m * k(k為非負(fù)整數(shù))。
若某字段的對(duì)齊要求為 2^n(n≤m),其偏移量必為 2^n 的整數(shù)倍,記為 2^n * f(f為非負(fù)整數(shù))。
則該字段實(shí)際地址為:
結(jié)構(gòu)體起始地址 + 字段偏移量 = (2^m * k) + (2^n * f)
由于 2^m 必定可以被 2^n 整除(因?yàn)?n≤m),所以無(wú)論 k 和 f 取何值,上述字段地址總能被 2^n 整除。這就保證了該字段的地址總是滿足其對(duì)齊要求。
因此,只要給每個(gè)字段的 偏移量 選擇其 對(duì)齊要求 的整數(shù)倍,就能保證結(jié)構(gòu)體任何實(shí)例、任意字段的地址都天然對(duì)齊,而無(wú)需依賴結(jié)構(gòu)體起始地址的額外信息。
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;
}
輸出結(jié)果如下:
a offset: 0
b offset: 8
c offset: 16
填充(Padding)分為兩部分:
字段之間的填充:為了滿足對(duì)齊要求,.NET 可能會(huì)在字段之間插入填充字節(jié)。字段之間的填充由字段的偏移量決定。
結(jié)構(gòu)體末尾的填充:為了確保結(jié)構(gòu)體的大小是最大字段對(duì)齊要求的倍數(shù),.NET 可能會(huì)在結(jié)構(gòu)體末尾添加填充字節(jié)。末尾填充保證了數(shù)組中連續(xù)的結(jié)構(gòu)體實(shí)例在內(nèi)存中也滿足對(duì)齊要求。
借助 ObjectLayoutInspector
庫(kù),我們可以觀察到結(jié)構(gòu)體的內(nèi)存布局,包括字段之間的填充和結(jié)構(gòu)體末尾的填充。
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;
}
輸出結(jié)果如下:
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
雖然包含了相同類型的字段,但由于字段的順序不同,導(dǎo)致它們的內(nèi)存布局和填充字節(jié)數(shù)量也不同。Foo
需要在在末尾添加 7 字節(jié)的填充才能滿足其大小是最大字段對(duì)齊要求的倍數(shù)。
包含引用類型字段的結(jié)構(gòu)體的默認(rèn)字段布局
如果結(jié)構(gòu)體包含引用類型字段,則該結(jié)構(gòu)體的默認(rèn)布局為 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
控制字段布局
在某些情況下,我們可能需要控制結(jié)構(gòu)體字段的內(nèi)存布局,以滿足特定的性能要求或與非托管代碼交互。可以使用 StructLayoutAttribute
特性來(lái)控制結(jié)構(gòu)體的內(nèi)存布局。
StructLayoutAttribute
有兩個(gè)重要的屬性:
LayoutKind
:指定結(jié)構(gòu)體的布局方式,可以是 Sequential
(按聲明順序排列)、Explicit
(顯式指定字段偏移量)或 Auto
(自動(dòng)布局)。
Pack
:指定結(jié)構(gòu)體及其字段的對(duì)齊要求,其值必須為 0、1、2、4、8、16、32、64 或 128,否則無(wú)法編譯成功,默認(rèn)值為 0。** 指定 Pack
> 8 時(shí), 等效于 Pack = 8
,因?yàn)槟壳鞍姹緵](méi)有任何類型的對(duì)齊要求超過(guò) 8 字節(jié)。**
Pack
屬性在 LayoutKind.Auto
布局中無(wú)效。在 LayoutKind.Sequential
布局中,Pack
屬性用于指定字段的對(duì)齊要求及結(jié)構(gòu)體實(shí)例的對(duì)齊要求;在 LayoutKind.Explicit
布局中,Pack
屬性用于結(jié)構(gòu)體的對(duì)齊要求,會(huì)影響結(jié)構(gòu)體實(shí)例的末尾填充。
LayoutKind.Sequential#
Pack 為 0 時(shí)等于默認(rèn)布局#
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 時(shí),取 Pack 和 字段類型大小 的較小值#
Pack
設(shè)置為 4 時(shí),int
和 long
字段的對(duì)齊要求都將被設(shè)置為 4 字節(jié),而 byte
字段的對(duì)齊要求仍然是 1 字節(jié)。結(jié)構(gòu)體的對(duì)齊要求是 4 字節(jié)。
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) |
|==========================|
結(jié)構(gòu)體實(shí)例的起始地址按 8 字節(jié) 對(duì)齊(782597876240 / 8 = 97824734530)。但其大小取滿足 4 字節(jié)對(duì)齊要求的最小整數(shù)倍 16 字節(jié)( 末尾字段 c 的偏移量為 12,最小只能取到 16),并在末尾添加 3 字節(jié)的填充。
Pack 設(shè)置為 1 時(shí),會(huì)形成密集的字段布局#
當(dāng) Pack
設(shè)置為 1 時(shí),所有字段的對(duì)齊要求都將被設(shè)置為 1 字節(jié),這意味著結(jié)構(gòu)體實(shí)例將按照 1 字節(jié)對(duì)齊。此時(shí),結(jié)構(gòu)體實(shí)例的字段將緊密排列,不會(huì)有額外的填充字節(jié)。
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 的倍數(shù)(302463314288 / 8 = 37807914286),但結(jié)構(gòu)體實(shí)例的大小變?yōu)?13 字節(jié),沒(méi)有末尾填充。
Pack 不為 0 的結(jié)構(gòu)體作為其他結(jié)構(gòu)體字段時(shí)#
如果外層的結(jié)構(gòu)體采用默認(rèn)字段布局則,則其實(shí)例的起始地址取決嵌套結(jié)構(gòu)體的最大字段默認(rèn)對(duì)齊要求,其實(shí)例大小取決該結(jié)構(gòu)體的最大字段對(duì)齊要求。
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
結(jié)構(gòu)體包含一個(gè) Foo
類型的字段 foo
。
Bar
的實(shí)例起始地址也滿足 8 字節(jié)對(duì)齊要求(724703897336 / 8 = 90587987167)。
foo
的對(duì)齊要求為 1 字節(jié), d
的對(duì)齊要求為 4 字節(jié),所以 Bar
的實(shí)例大小為 20 字節(jié)(4 的整數(shù)倍),并在foo
和 d
之間添加了 3 字節(jié)的填充。
LayoutKind.Explicit#
Pack 為 0 時(shí),結(jié)構(gòu)體按照最大字段默認(rèn)對(duì)齊要求對(duì)齊#
在 Explicit
布局中,我們需要顯式指定每個(gè)字段的偏移量。使用 FieldOffsetAttribute
來(lái)指定字段的偏移量。此時(shí)偏移量可以是任意值,甚至允許重疊字段。
此時(shí)雖然字段地址可能由于是任意值而不滿足對(duì)齊要求,但結(jié)構(gòu)體實(shí)例的起始地址依舊按 8 字節(jié) 對(duì)齊,且結(jié)構(gòu)體實(shí)例的大小是最大字段對(duì)齊要求的整數(shù)倍。
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
結(jié)構(gòu)體的字段 a
、b
和 c
的偏移量分別為 0、3 和 11??梢钥吹?,雖然字段的地址不再滿足對(duì)齊要求,但結(jié)構(gòu)體實(shí)例的起始地址仍然按 8 字節(jié) 對(duì)齊(6095151432 / 8 = 761893929),且結(jié)構(gòu)體實(shí)例的大小為 16 字節(jié)(最大字段對(duì)齊要求的整數(shù)倍),末尾添加了 4 字節(jié)的填充。
如果將 c
字段的偏移量改為 16,則結(jié)構(gòu)體實(shí)例的大小將變?yōu)?24 字節(jié),并且會(huì)在末尾添加 7 字節(jié)的填充。
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 時(shí),結(jié)構(gòu)體實(shí)例按照 Pack 與 最大字段對(duì)齊要求 的較小值對(duì)齊#
在 Explicit
布局中,如果設(shè)置了 Pack
屬性且不為 0,則結(jié)構(gòu)體實(shí)例將按照 Pack
的值對(duì)齊。字段的偏移量仍然可以是任意值,但結(jié)構(gòu)體實(shí)例的大小將受到 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
屬性設(shè)置為 4,與 long 類型的 8 字節(jié) 比較則結(jié)構(gòu)體實(shí)例應(yīng)按照 4 字節(jié) 對(duì)齊。因?yàn)?nbsp;c
的偏移量為 16,所以 Foo
的大小在取值此時(shí)符合條件的 4 的最小整數(shù)倍后變?yōu)?20 字節(jié),并在末尾添加了 3 字節(jié) 的填充。
改成 Pack = 128
后,結(jié)構(gòu)體實(shí)例的大小按照最大字段默認(rèn)對(duì)齊要求 8 字節(jié) 對(duì)齊。
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 屬性設(shè)置為 1 可以消除結(jié)構(gòu)體實(shí)例的末尾填充#
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) |
|==========================|
此時(shí)實(shí)例的起始地址仍然按 8 字節(jié) 對(duì)齊(468685679112 / 8 = 58585709889),但實(shí)例的大小則是 17 字節(jié),末尾填充為 0 字節(jié)。
Pack 屬性不為 0 的結(jié)構(gòu)體作為其他結(jié)構(gòu)體字段時(shí)#
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
結(jié)構(gòu)體包含一個(gè) Foo
類型的字段 foo
,由于 Foo
的最大字段對(duì)齊要求為 8 字節(jié),所以 Bar
的實(shí)例起始地址也滿足 8 字節(jié)對(duì)齊要求(967090628200 / 8 = 120886328525)。
foo
的對(duì)齊要求為 1 字節(jié), d
的對(duì)齊要求為 4 字節(jié),所以 Bar
的實(shí)例大小為 24 字節(jié)(4 的整數(shù)倍),并在foo
和 d
之間添加了 3 字節(jié)的填充。
LayoutKind.Auto#
使用 LayoutKind.Auto
時(shí),runtime 將根據(jù)字段的類型和聲明順序自動(dòng)確定字段的布局,會(huì)調(diào)整實(shí)例字段的排列順序和對(duì)齊要求,以優(yōu)化內(nèi)存布局和性能。
LayoutKind.Auto
也是引用類型實(shí)例字段的默認(rèn)布局方式。
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
結(jié)構(gòu)體的字段 b
被放在了前面,各字段都按照其類型大小進(jìn)行了對(duì)齊,相較于默認(rèn)布局,Foo
結(jié)構(gòu)體的內(nèi)存布局更加緊湊,減少了填充字節(jié)的數(shù)量。
等效于于下面的結(jié)構(gòu)體定義
[StructLayout(LayoutKind.Sequential, Pack = 0)]
struct Foo
{
public long b;
public int a;
public byte c;
}
作為數(shù)組元素時(shí)的結(jié)構(gòu)體實(shí)例
默認(rèn)字段布局#
默認(rèn)布局的結(jié)構(gòu)體實(shí)例在數(shù)組中也會(huì)按照最大字段對(duì)齊要求進(jìn)行對(duì)齊。每個(gè)結(jié)構(gòu)體實(shí)例的起始地址都是該結(jié)構(gòu)體最大字段對(duì)齊要求的整數(shù)倍。
因此默認(rèn)布局下,數(shù)組中的每個(gè)結(jié)構(gòu)體的字段都是滿足對(duì)齊要求的。
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 |
非默認(rèn)字段布局#
因?yàn)閿?shù)組中結(jié)構(gòu)體實(shí)例是連續(xù)存儲(chǔ)的,如果結(jié)構(gòu)體實(shí)例的字段布局進(jìn)行了非默認(rèn)的調(diào)整,則可能導(dǎo)致第二個(gè)開始的構(gòu)體實(shí)例完全不滿足對(duì)齊要求(包括實(shí)例的起始地址和字段地址)。
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 |