目录
- 前言
- 基本概念
- 结构体的默认字段布局
- 包含引用类型字段的结构体的默认字段布局
- [用
StructLayoutAttribute
控制字段布局](#用 StructLayoutAttribute 控制字段布局)- LayoutKind.Sequential
- [Pack 为 0 时等于默认布局](#Pack 为 0 时等于默认布局)
- [Pack 不为 0 时,取 Pack 和 字段类型大小 的较小值](#Pack 不为 0 时,取 Pack 和 字段类型大小 的较小值)
- [Pack 设置为 1 时,会形成密集的字段布局](#Pack 设置为 1 时,会形成密集的字段布局)
- [Pack 不为 0 的结构体作为其他结构体字段时](#Pack 不为 0 的结构体作为其他结构体字段时)
- LayoutKind.Explicit
- [Pack 为 0 时,结构体按照最大字段默认对齐要求对齐](#Pack 为 0 时,结构体按照最大字段默认对齐要求对齐)
- [Pack 不为 0 时,结构体实例按照 Pack 与 最大字段对齐要求 的较小值对齐](#Pack 不为 0 时,结构体实例按照 Pack 与 最大字段对齐要求 的较小值对齐)
- [将 Pack 属性设置为 1 可以消除结构体实例的末尾填充](#将 Pack 属性设置为 1 可以消除结构体实例的末尾填充)
- [Pack 属性不为 0 的结构体作为其他结构体字段时](#Pack 属性不为 0 的结构体作为其他结构体字段时)
- LayoutKind.Auto
- LayoutKind.Sequential
- 作为数组元素时的结构体实例
前言
大部分情况下我们并不需要关心结构体字段的内存布局,但是在一些特殊情况下,比如性能优化、和非托管代码交互、对结构体进行序列化等场景下,了解字段的内存布局是非常重要的。
本文写作时 最新的 .NET 正式版是 .NET 9,以后的版本不保证本文内容的准确性,仅供参考。
本文将介绍 .NET 中结构体字段的内存布局,包括字段的对齐(Alignment)、填充(Padding)以及如何使用 StructLayoutAttribute
来控制字段的内存布局。
对齐的目的是为了 CPU 访问内存的效率,64 位系统和 32 位系统中对齐要求存在差异,下文如果没有特别说明,均指 64 位系统。
填充则是为了满足对齐要求而在字段之间或结构体末尾添加的额外字节。
结构体的对其规则同时适用于栈上和堆上的结构体结构体实例,方便起见,大部分例子将使用栈上结构体实例来演示。
一些资料是从 字段的偏移量(offset)为出发点来介绍字段的内存布局的,但笔者认为从字段的 内存地址 出发更容易理解。
由于一些资料并没有找到明确的官方的解释,笔者是在实验和推导的基础上总结出这些规则的,可能会有不准确的地方,欢迎读者在评论区指出。
本文虽然没有直接介绍引用类型的字段布局,但引用类型实例的字段的内存布局概念与结构体实例的内存布局是相同的。不同之处在于引用类型的默认布局是 LayoutKind.Auto
,而结构体的默认布局是 LayoutKind.Sequential
。读者可以自己尝试观察引用类型实例字段的内存布局。
本文将使用下面的方法来观察字段的内存地址:
csharp
// 打印日志头
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
csharp
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 字节对齐。
bash
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) |
|===============================|
下面是一个简单的示例,展示了结构体字段的默认布局:
csharp
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;
}
输出结果如下:
bash
| 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 位系统上运行下面的代码:
csharp
using System.Runtime.CompilerServices;
using ObjectLayoutInspector;
unsafe
{
var foo = new Foo();
var bar = new Bar();
// 方法 PrintPointerHeader 和 PrintPointerDetails 在前言部分已经定义
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;
}
输出结果如下:
bash
| 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 位的环境中:
xml
<PropertyGroup>
<RuntimeIdentifier>win-x86</RuntimeIdentifier>
</PropertyGroup>
下面是 intel CPU 的输出结果:
bash
| 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 的输出结果:
csharp
运行上述代码,输出结果如下:
```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 整除。这就保证了该字段的地址总是满足其对齐要求。
因此,只要给每个字段的 偏移量 选择其 对齐要求 的整数倍,就能保证结构体任何实例、任意字段的地址都天然对齐,而无需依赖结构体起始地址的额外信息。
csharp
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;
}
输出结果如下:
bash
a offset: 0
b offset: 8
c offset: 16
填充
填充(Padding)分为两部分:
-
字段之间的填充:为了满足对齐要求,.NET 可能会在字段之间插入填充字节。字段之间的填充由字段的偏移量决定。
-
结构体末尾的填充:为了确保结构体的大小是最大字段对齐要求的倍数,.NET 可能会在结构体末尾添加填充字节。末尾填充保证了数组中连续的结构体实例在内存中也满足对齐要求。
借助 ObjectLayoutInspector
库,我们可以观察到结构体的内存布局,包括字段之间的填充和结构体末尾的填充。
csharp
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;
}
输出结果如下:
bash
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
。
csharp
using ObjectLayoutInspector;
TypeLayout.PrintLayout<Foo>();
struct Foo
{
public int a;
public string b;
public byte c;
}
bash
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 时等于默认布局
csharp
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;
}
bash
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 字节。
csharp
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using ObjectLayoutInspector;
{
var foo = new Foo();
// 方法 PrintPointerHeader 和 PrintPointerDetails 在前言部分已经定义
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;
}
bash
| 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 字节对齐。此时,结构体实例的字段将紧密排列,不会有额外的填充字节。
csharp
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using ObjectLayoutInspector;
var foo = new Foo
{
a = 1,
b = 2,
c = 3
};
unsafe
{
// 方法 PrintPointerHeader 和 PrintPointerDetails 在前言部分已经定义
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;
}
bash
| 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 的结构体作为其他结构体字段时
如果外层的结构体采用默认字段布局则,则其实例的起始地址取决嵌套结构体的最大字段默认对齐要求,其实例大小取决该结构体的最大字段对齐要求。
csharp
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;
}
bash
| 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 字节 对齐,且结构体实例的大小是最大字段对齐要求的整数倍。
csharp
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using ObjectLayoutInspector;
var foo = new Foo
{
a = 1,
b = 2,
c = 3
};
unsafe
{
// 方法 PrintPointerHeader 和 PrintPointerDetails 在前言部分已经定义
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;
}
bash
| 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 字节的填充。
csharp
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>();
bash
| 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
属性的影响。
csharp
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;
}
bash
| 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 字节 对齐。
csharp
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;
}
bash
| 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 可以消除结构体实例的末尾填充
csharp
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;
}
bash
| 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 的结构体作为其他结构体字段时
csharp
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;
}
bash
| 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
也是引用类型实例字段的默认布局方式。
csharp
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;
}
bash
| 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
结构体的内存布局更加紧凑,减少了填充字节的数量。
等效于于下面的结构体定义
csharp
[StructLayout(LayoutKind.Sequential, Pack = 0)]
struct Foo
{
public long b;
public int a;
public byte c;
}
作为数组元素时的结构体实例
默认字段布局
默认布局的结构体实例在数组中也会按照最大字段对齐要求进行对齐。每个结构体实例的起始地址都是该结构体最大字段对齐要求的整数倍。
因此默认布局下,数组中的每个结构体的字段都是满足对齐要求的。
csharp
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;
}
bash
| 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 |
非默认字段布局
因为数组中结构体实例是连续存储的,如果结构体实例的字段布局进行了非默认的调整,则可能导致第二个开始的构体实例完全不满足对齐要求(包括实例的起始地址和字段地址)。
csharp
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;
}
bash
| 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 |