详情

首页手游攻略 .NET 结构体字段的内存布局深度解析实用指南

.NET 结构体字段的内存布局深度解析实用指南

佚名 2026-09-07 10:40:01

平时做技术实践时,很多问题不是概念不会,而是细节没串起来。拿“.NET 结构体字段的内存布局深度解析”来说,它看着像小点,放到项目里常会牵出环境、配置、兼容性和维护成本。下面按实际采用顺序,把思路、关键写法和容易踩坑的地方讲清楚,便于大家直接对照操作。

目录
  • 前言
  • 基本概念
  • 结构体的默认字段布局
    • 对齐
    • 64 位系统与 32 位系统的对齐要求差异
    • 默认字段布局中 对齐要求 与 偏移量 的关系
  • 填充
    • 包含引用类型字段的结构体的默认字段布局
      • 用StructLayoutAttribute控制字段布局
        • LayoutKind.Sequential
          • Pack 为 0 时等于默认布局
          • Pack 不为 0 时,取 Pack 和 字段类型大小 的较小值
          • Pack 设置为 1 时,会形成密集的字段布局
          • Pack 不为 0 的结构体作为其他结构体字段时
        • LayoutKind.Explicit
          • Pack 为 0 时,结构体按照最大字段默认对齐要求对齐
          • 理解这一步时,Pack 不为 0 时,结构体实例按照 Pack 与 最大字段对齐要求 的较小值对齐
          • 将 Pack 属性设置为 1 能够消除结构体实例的末尾填充
          • Pack 属性不为 0 的结构体作为其他结构体字段时
        • LayoutKind.Auto
          • 作为数组元素时的结构体实例
            • 默认字段布局
            • 非默认字段布局

          前言

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

          在这个场景下,本文写作时 最新的 .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 这个开源库来观察字段的内存布局。

          项目地址:(链接已移除)

          nuget 包地址:(链接已移除)

          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 结构体,它有两个字段 ab,分别是 intlong 类型,对齐要求分别是 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.abar.foo.b 的地址也满足各自的对齐要求。

          Baz 类包含一个 Foo 类型的字段 foo,由于 Baz 是引用类型,所以它的实例在堆上分配内存。BazFoo 类型字段也依旧需满足 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 在前言部分已经定义
              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) |
          |==========================|

          能够看到,FooBar 结构体的实例大小分别为 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) |
          |==========================|

          FooBar 结构体的起始地址和字段地址都只满足 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) |
          |==========================|

          FooBar 虽然包含了相同类型的字段,但由于字段的顺序不同,导致它们的内存布局和填充字节数量也不同。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 时,intlong 字段的对齐要求都将被设置为 4 字节,而 byte 字段的对齐要求仍然是 1 字节。结构体的对齐要求是 4 字节。

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

          | 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 在前言部分已经定义
              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 的整数倍),同时在food 之间添加了 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 在前言部分已经定义
              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 结构体的字段 abc 的偏移量分别为 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 的整数倍),同时在food 之间添加了 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 |

          到此这篇关于.NET 结构体字段的内存布局深度解析的文章就介绍到这了,更多相关.net结构体字段的内存布局内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多兼容脚本之家!

          您可能感兴趣的文章:

          • 深入理解.NET对象的内存布局
          • .NET Core内存结构体系(Windows环境)底层原理解析
          • .NET采用结构体替代类提升性能优化的技巧
          • .NET性能优化之为结构体数组采用StructLinq的问题解析

          相关资讯
          点击查看更多
          游戏推荐
          推荐专题
          热门阅读
          推荐下载