理解 .NET 结构体字段的内存布局

B站影视 内地电影 2025-06-06 10:13 1

摘要:大部分情况下我们并不需要关心结构体字段的内存布局,但是在一些特殊情况下,比如性能优化、和非托管代码交互、对结构体进行序列化等场景下,了解字段的内存布局是非常重要的。

大部分情况下我们并不需要关心结构体字段的内存布局,但是在一些特殊情况下,比如性能优化、和非托管代码交互、对结构体进行序列化等场景下,了解字段的内存布局是非常重要的。

本文写作时 最新的 .NET 正式版是 .NET 9,以后的版本不保证本文内容的准确性,仅供参考。

本文将介绍 .NET 中结构体字段的内存布局,包括字段的对齐(Alignment)、填充(padding)以及如何使用StructLayoutAttribute来控制字段的内存布局。

对齐的目的是为了 CPU 访问内存的效率,64 位系统和 32 位系统中对齐要求存在差异,下文如果没有特别说明,均指 64 位系统。

填充则是为了满足对齐要求而在字段之间或结构体末尾添加的额外字节。

结构体的对其规则同时适用于栈上和堆上的结构体结构体实例,方便起见,大部分例子将使用栈上结构体实例来演示。

一些资料是从 字段的偏移量(offset)为出发点来介绍字段的内存布局的,但笔者认为从字段的 内存地址 出发更容易理解。

由于一些资料并没有找到明确的官方的解释,笔者是在实验和推导的基础上总结出这些规则的,可能会有不准确的地方,欢迎读者在评论区指出。

本文虽然没有直接介绍引用类型的字段布局,但引用类型实例的字段的内存布局概念与结构体实例的内存布局是相同的。不同之处在于引用类型的默认布局是LayoutKind.Auto,而结构体的默认布局是LayoutKind.Sequential。读者可以自己尝试观察引用类型实例字段的内存布局。

本文将使用下面的方法来观察字段的内存地址:

// 打印日志头
voidPrintPointerHeader
{
Console.WriteLine(
$"| {"Expr"-15} | {"Address"-15} | {"Size"-4} | {"AlignedBySize"-13} | {"Addr/Size"-12} |");
}

// 打印指针的详细信息
unsafevoidPrintPointerDetails(
T* ptr,
[CallerArgumentExpression("ptr")]string? pointerExpr = )
whereT : unmanaged
{
ulongaddressValue = (ulong)ptr;
ulongtypeSize = (ulong)sizeof(T);

decimaladdressDivBySize = addressValue / (decimal)typeSize;
boolisAlignedBySize = 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 addpackage ObjectLayoutInspector --version0.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) |

下面是一个简单的示例,展示了结构体字段的默认布局:

usingSystem.Runtime.CompilerServices;

varfoo =newFoo;
varbar =newBar;
varbaz =newBaz;

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);
}
}

structFoo
{
publicinta;
publiclongb;
}

structBar
{
publicFoo foo;
}

classBaz
{
publicFoo 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结构体,它有两个字段along所以Foo实例在栈上的地址按照 8 字节 对齐(6095528264 / 8 = 761941033)。字段是foo的第一个字段,它的地址也就是foo的起始地址,自然也满足int的对齐要求(6095528264 / 4 = 1523882066)。字段是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 位系统上运行下面的代码:

usingSystem.Runtime.CompilerServices;
usingObjectLayoutInspector;

unsafe
{
varfoo =newFoo;
varbar =newBar;

// 方法 PrintPointerHeader 和 PrintPointerDetails 在前言部分已经定义
PrintPointerHeader;
PrintPointerDetails(&foo.a);
PrintPointerDetails(&foo.b);
PrintPointerDetails(&foo.c);
PrintPointerDetails(&bar.d);
PrintPointerDetails(&bar.e);
PrintPointerDetails(&bar.f);
}

TypeLayout.PrintLayout;
TypeLayout.PrintLayout;

structFoo
{
publicinta;
publiclongb;
publicbytec;
}

structBar
{
publicintd;
publicinte;
publicbytef;
}

输出结果如下:

| 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 layoutfor'Foo'
Size: 24 bytes. Paddings: 11 bytes (E 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 layoutfor'Bar'
Size: 12 bytes. Paddings: 3 bytes (% 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
RuntimeIdentifierwin-x86RuntimeIdentifier
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 layoutfor'Foo'
Size: 24 bytes. Paddings: 11 bytes (E 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 layoutfor'Bar'
Size: 12 bytes. Paddings: 3 bytes (% 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 layoutfor'Foo'
Size:24bytes. Paddings:11bytes (%45of empty space)

|0 -3: Int32a(4bytes) |
||
| 4-7:padding(4bytes) |

| 8-15: Int64b(8bytes) |

| 16: Bytec(1byte) |

| 17-23:padding(7bytes) |

Type layoutfor'Bar'
Size:12bytes. Paddings:3bytes (%25of empty space)

|0 -3: Int32d(4bytes) |

| 4-7: Int32e(4bytes) |

| 8: Bytef(1byte) |

| 9-11:padding(3bytes) |

Foo的起始地址仍然满足 8 字节对齐要求,但Bar的起始地址不再满足 8 字节对齐要求(47706548 / 8 = 5963318.5),而是满足 4 字节对齐要求(47706548 / 4 = 11926637)。默认字段布局中 对齐要求 与 偏移量 的关系

偏移量(offset)是指字段相对于结构体实例起始地址的距离,决定了字段在内存中的位置。

偏移量的值取决于对齐要求和字段的顺序,会在未被顺序在前的字段占用的内存空间中取对齐要求的最小整数倍。

下面几个设计确保了不管结构体实例的起始地址如何,任意一个字段只要给定一个满足对齐要求的偏移量,就可以满足该字段的对齐要求:

对齐要求总是 2 的整数次幂。

实例的起始地址(按 8 字节 对齐)总是满足最大字段的对齐要求。

偏移量的值是对齐要求的整数倍

下面做一个简单的推导来帮助读者理解:

假设结构体中最大字段的对齐要求为 2^m(m 为

若某字段的对齐要求为 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
{
varfoo =newFoo;

varaddr = (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}");
}

structFoo
{
publicinta;
publiclongb;
publicbytec;
}

输出结果如下:

a offset: 0
b offset: 8
c offset: 16
填充

填充(Padding)分为两部分:

字段之间的填充:为了满足对齐要求,.NET 可能会在字段之间插入填充字节。字段之间的填充由字段的偏移量决定。

结构体末尾的填充:为了确保结构体的大小是最大字段对齐要求的倍数,.NET 可能会在结构体末尾添加填充字节。末尾填充保证了数组中连续的结构体实例在内存中也满足对齐要求。

借助ObjectLayoutInspector库,我们可以观察到结构体的内存布局,包括字段之间的填充和结构体末尾的填充。usingObjectLayoutInspector;

TypeLayout.PrintLayout;
TypeLayout.PrintLayout;

structFoo
{
publicinta;
publiclongb;
publicbytec;
}

structBar
{
publicbytec;
publicinta;
publiclongb;
}

输出结果如下:

Type layout for'Foo'
Size: 24 bytes. Paddings: 11 bytes (E 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 layoutfor'Bar'
Size: 16 bytes. Paddings: 3 bytes ( of empty space)

| 0: Byte c (1 byte) |

| 1-3: padding (3 bytes) |

| 4-7: Int32 a (4 bytes) |

FooBar虽然包含了相同类型的字段,但由于字段的顺序不同,导致它们的内存布局和填充字节数量也不同。Foo需要在在末尾添加 7 字节的填充才能满足其大小是最大字段对齐要求的倍数。如果结构体包含引用类型字段,则该结构体的默认布局为LayoutKind.Auto。usingObjectLayoutInspector;

TypeLayout.PrintLayout;

structFoo
{
publicinta;
publicstringb;
publicbytec;
}
Type layout for'Foo'
Size: 16 bytes. Paddings: 3 bytes ( 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) |

在某些情况下,我们可能需要控制结构体字段的内存布局,以满足特定的性能要求或与非托管代码交互。可以使用特性来控制结构体的内存布局。

LayoutKind:指定结构体的布局方式,可以是Sequential(按声明顺序排列)、Explicit(显式指定字段偏移量)或Auto(自动布局)。

Pack:指定结构体及其字段的对齐要求,其值必须为 0、1、2、4、8、16、32、64 或 128,否则无法编译成功,默认值为 0。** 指定Pack> 8 时, 等效于Pack = 8,因为目前版本没有任何类型的对齐要求超过 8 字节。**

Pack属性在LayoutKind.Auto布局中无效。在布局中,Pack属性用于指定字段的对齐要求及结构体实例的对齐要求;在LayoutKind.Explicit布局中,Pack属性用于结构体的对齐要求,会影响结构体实例的末尾填充。LayoutKind.SequentialPack 为 0 时等于默认布局usingSystem.Runtime.InteropServices;
usingObjectLayoutInspector;

TypeLayout.PrintLayout;

[StructLayout(LayoutKind.Sequential, Pack = 0)]
structFoo
{
publicinta;
publiclongb;
publicbytec;
}
Type layout for'Foo'
Size: 24 bytes. Paddings: 11 bytes (E 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 字节。usingSystem.Runtime.CompilerServices;
usingSystem.Runtime.InteropServices;
usingObjectLayoutInspector;

{
varfoo =newFoo;

// 方法 PrintPointerHeader 和 PrintPointerDetails 在前言部分已经定义
PrintPointerHeader;
PrintPointerDetails(&foo.a);
PrintPointerDetails(&foo.b);
PrintPointerDetails(&foo.c);
}

TypeLayout.PrintLayout;

[StructLayout(LayoutKind.Sequential, Pack = 4)]
structFoo
{
publicinta;
publiclongb;
publicbytec;
}
| 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 layoutfor'Foo'
Size: 16 bytes. Paddings: 3 bytes ( 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 字节对齐。此时,结构体实例的字段将紧密排列,不会有额外的填充字节。usingSystem.Runtime.CompilerServices;
usingSystem.Runtime.InteropServices;
usingObjectLayoutInspector;

varfoo =newFoo
{
a =1
b =2
c =3
};

unsafe
{
// 方法 PrintPointerHeader 和 PrintPointerDetails 在前言部分已经定义
PrintPointerHeader;
PrintPointerDetails(&foo.a);
PrintPointerDetails(&foo.b);
PrintPointerDetails(&foo.c);
}

TypeLayout.PrintLayout;

[StructLayout(LayoutKind.Sequential, Pack = 1)]
structFoo
{
publicinta;
publiclongb;
publicbytec;
}
| 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 layoutfor'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 的结构体作为其他结构体字段时

如果外层的结构体采用默认字段布局则,则其实例的起始地址取决嵌套结构体的最大字段默认对齐要求,其实例大小取决该结构体的最大字段对齐要求。

usingSystem.Runtime.CompilerServices;
usingSystem.Runtime.InteropServices;
usingObjectLayoutInspector;

unsafe
{
PrintPointerHeader;
PrintPointerDetails(&bar.foo.a);
PrintPointerDetails(&bar.foo.b);
PrintPointerDetails(&bar.foo.c);
PrintPointerDetails(&bar.d);
}

TypeLayout.PrintLayout;

[StructLayout(LayoutKind.Sequential, Pack = 1)]
structFoo
{
publicinta;
publiclongb;
publicbytec;
}

structBar
{
publicFoo foo;
publicintd;
}
| 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 layoutfor'Bar'
Size: 20 bytes. Paddings: 3 bytes ( 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的实例起始地址也满足 8 字节对齐要求(724703897336 / 8 = 90587987167)。foo的对齐要求为 1 字节,的对齐要求为 4 字节,所以Bar的实例大小为 20 字节(4 的整数倍),并在foo之间添加了 3 字节的填充。LayoutKind.ExplicitPack 为 0 时,结构体按照最大字段默认对齐要求对齐在Explicit布局中,我们需要显式指定每个字段的偏移量。使用FieldOffsetAttribute来指定字段的偏移量。此时偏移量可以是任意值,甚至允许重叠字段。

此时虽然字段地址可能由于是任意值而不满足对齐要求,但结构体实例的起始地址依旧按 8 字节 对齐,且结构体实例的大小是最大字段对齐要求的整数倍。

usingSystem.Runtime.CompilerServices;
usingSystem.Runtime.InteropServices;
usingObjectLayoutInspector;

varfoo =newFoo
{
a =1
b =2
c =3
};

unsafe
{
// 方法 PrintPointerHeader 和 PrintPointerDetails 在前言部分已经定义
PrintPointerHeader;
PrintPointerDetails(&foo.a);
PrintPointerDetails(&foo.b);
PrintPointerDetails(&foo.c);
}

TypeLayout.PrintLayout;

[StructLayout(LayoutKind.Explicit, Pack = 0)]

structFoo
{
[FieldOffset(0)]
publicinta;
[FieldOffset(3)]
publiclongb;
[FieldOffset(11)]
publicbytec;
}
| 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 layoutfor'Foo'
Size: 16 bytes. Paddings: 4 bytes (% 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) |

上面例子中,结构体的字段的偏移量分别为 0、3 和 11。可以看到,虽然字段的地址不再满足对齐要求,但结构体实例的起始地址仍然按 8 字节 对齐(6095151432 / 8 = 761893929),且结构体实例的大小为 16 字节(最大字段对齐要求的整数倍),末尾添加了 4 字节的填充。字段的偏移量改为 16,则结构体实例的大小将变为 24 字节,并且会在末尾添加 7 字节的填充。usingSystem.Runtime.CompilerServices;
usingSystem.Runtime.InteropServices;
usingObjectLayoutInspector;

varfoo =newFoo
{
a =1
b =2
c =3
};

unsafe
{
PrintPointerHeader;
PrintPointerDetails(&foo.a);
PrintPointerDetails(&foo.b);
PrintPointerDetails(&foo.c);
}

TypeLayout.PrintLayout;
| 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 layoutfor'Foo'
Size: 24 bytes. Paddings: 12 bytes (P 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属性的影响。varfoo =newFoo
{
a =1
b =2
c =3
};

unsafe
{
PrintPointerHeader;
PrintPointerDetails(&foo.a);
PrintPointerDetails(&foo.b);
PrintPointerDetails(&foo.c);
}

TypeLayout.PrintLayout;

[StructLayout(LayoutKind.Explicit, Pack = 4)]
structFoo
{
[FieldOffset(0)]
publicinta;
[FieldOffset(5)]
publiclongb;
[FieldOffset(16)]
publicbytec;
}
| 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 layoutfor'Foo'
Size: 20 bytes. Paddings: 7 bytes (5 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 字节 对齐。varfoo =newFoo
{
a =1
b =2
c =3
};

unsafe
{
PrintPointerHeader;
PrintPointerDetails(&foo.a);
PrintPointerDetails(&foo.b);
PrintPointerDetails(&foo.c);
}

TypeLayout.PrintLayout;

[StructLayout(LayoutKind.Explicit, Pack = 128)]
structFoo
{
[FieldOffset(0)]
publicinta;
[FieldOffset(5)]
publiclongb;
[FieldOffset(16)]
publicbytec;
}
| 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 layoutfor'Foo'
Size: 24 bytes. Paddings: 11 bytes (E 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 可以消除结构体实例的末尾填充usingSystem.Runtime.CompilerServices;
usingSystem.Runtime.InteropServices;
usingObjectLayoutInspector;

varfoo =newFoo
{
a =1
b =2
c =3
};
unsafe
{
PrintPointerHeader;
PrintPointerDetails(&foo.a);
PrintPointerDetails(&foo.b);
PrintPointerDetails(&foo.c);
}

TypeLayout.PrintLayout;

[StructLayout(LayoutKind.Explicit, Pack = 1)]
structFoo
{
[FieldOffset(0)]
publicinta;
[FieldOffset(5)]
publiclongb;
[FieldOffset(16)]
publicbytec;
}
| 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 layoutfor'Foo'
Size: 17 bytes. Paddings: 4 bytes (# 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 的结构体作为其他结构体字段时usingSystem.Runtime.CompilerServices;
usingSystem.Runtime.InteropServices;
usingObjectLayoutInspector;

varbar =newBar
{
foo =newFoo,
d =4
};

unsafe
{
PrintPointerHeader;
PrintPointerDetails(&bar.foo.a);
PrintPointerDetails(&bar.foo.b);
PrintPointerDetails(&bar.foo.c);
PrintPointerDetails(&bar.d);
}

TypeLayout.PrintLayout;

[StructLayout(LayoutKind.Explicit, Pack = 1)]
structFoo
{
[FieldOffset(0)]
publicinta;
[FieldOffset(5)]
publiclongb;
[FieldOffset(16)]
publicbytec;
}

structBar
{
publicFoo foo;
publicintd;
}
| 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 layoutfor'Bar'
Size: 24 bytes. Paddings: 7 bytes 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) |

在上面的例子中,结构体包含一个Foo类型的字段foo,由于的最大字段对齐要求为 8 字节,所以的实例起始地址也满足 8 字节对齐要求(967090628200 / 8 = 120886328525)。foo的对齐要求为 1 字节,的对齐要求为 4 字节,所以Bar的实例大小为 24 字节(4 的整数倍),并在foo之间添加了 3 字节的填充。LayoutKind.Auto使用LayoutKind.Auto时,runtime 将根据字段的类型和声明顺序自动确定字段的布局,会调整实例字段的排列顺序和对齐要求,以优化内存布局和性能。LayoutKind.Auto也是引用类型实例字段的默认布局方式。usingSystem.Runtime.CompilerServices;
usingSystem.Runtime.InteropServices;
usingObjectLayoutInspector;

varfoo =newFoo
{
a =1
b =2
c =3
};

unsafe
{
PrintPointerHeader;
PrintPointerDetails(&foo.a);
PrintPointerDetails(&foo.b);
PrintPointerDetails(&foo.c);
}

TypeLayout.PrintLayout;

[StructLayout(LayoutKind.Auto)]
structFoo
{
publicinta;
publiclongb;
publicbytec;
}
| 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 layoutfor'Foo'
Size: 16 bytes. Paddings: 3 bytes ( 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结构体的内存布局更加紧凑,减少了填充字节的数量。

等效于于下面的结构体定义

[StructLayout(LayoutKind.Sequential, Pack = 0)]
structFoo
{
publiclongb;
publicinta;
publicbytec;
}
默认字段布局

默认布局的结构体实例在数组中也会按照最大字段对齐要求进行对齐。每个结构体实例的起始地址都是该结构体最大字段对齐要求的整数倍。

因此默认布局下,数组中的每个结构体的字段都是满足对齐要求的。

usingSystem.Runtime.CompilerServices;

unsafe
{
// 读者也可以替换成堆上分配的数组来查看运行结果
vararr =stackallocFoo {newFoo,newFoo };

PrintPointerHeader;

PrintPointerDetails(&arr[0].a);
0].b);
0].c);
1].a);
1].b);
1].c);
}

structFoo
{
publicinta;
publiclongb;
publicbytec;
}
| 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 |
非默认字段布局

因为数组中结构体实例是连续存储的,如果结构体实例的字段布局进行了非默认的调整,则可能导致第二个开始的构体实例完全不满足对齐要求(包括实例的起始地址和字段地址)。

usingSystem.Runtime.CompilerServices;

unsafe
{
// 读者也可以替换成堆上分配的数组来查看运行结果
vararr =stackallocFoo {newFoo,newFoo };

PrintPointerHeader;

PrintPointerDetails(&arr[0].a);
0].b);
0].c);
1].a);
1].b);
1].c);
}

[StructLayout(LayoutKind.Sequential, Pack = 1)]
structFoo
{
publicinta;
publiclongb;
publicbytec;
}
| 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 |

来源:opendotnet

相关推荐