CLR顺序结构布局:对齐和大小

con*_*low 5 .net c# clr struct value-type

struct默认情况下,C#中的所有内容都被视为已[StructLayout(LayoutKind.Sequential)]标记的值类型.所以我们拿一些structs来检查这个structs的大小:

using System;
using System.Reflection;
using System.Linq;
using System.Runtime.InteropServices;

class Foo
{
  struct E { }
  struct S0 { byte a; }
  struct S1 { byte a; byte b; }
  struct S2 { byte a; byte b; byte c; }
  struct S3 { byte a; int b; }
  struct S4 { int a; byte b; }
  struct S5 { byte a; byte b; int c; }
  struct S6 { byte a; int b; byte c; }
  struct S7 { int a; byte b; int c; }
  struct S8 { byte a; short b; int c; }
  struct S9 { short a; byte b; int c; }
  struct S10 { long a; byte b; }
  struct S11 { byte a; long b; }
  struct S12 { byte a; byte b; short c; short d; long e; }
  struct S13 { E a; E b; }
  struct S14 { E a; E b; int c; }
  struct S15 { byte a; byte b; byte c; byte d; byte e; }
  struct S16 { S15 b; byte c; }
  struct S17 { long a; S15 b; }
  struct S18 { long a; S15 b; S15 c; }
  struct S19 { long a; S15 b; S15 c; E d; short e; }
  struct S20 { long a; S15 b; S15 c; short d; E e; }

  static void Main()
  {
    Console.WriteLine("name: contents => size\n");
    foreach (var type in typeof(Foo).GetNestedTypes(BindingFlags.NonPublic))
    {
      var fields = type.GetFields(BindingFlags.NonPublic | BindingFlags.Instance);
      Console.WriteLine("{0}: {2} => {1}", type.Name, Marshal.SizeOf(type),
        string.Join("+", fields.Select(_ => Marshal.SizeOf(_.FieldType))));
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

输出是(x86/x64上相同):

name: contents => size

E:  => 1
S0: 1 => 1
S1: 1+1 => 2
S2: 1+1+1 => 3
S3: 1+4 => 8
S4: 4+1 => 8
S5: 1+1+4 => 8
S6: 1+4+1 => 12
S7: 4+1+4 => 12
S8: 1+2+4 => 8
S9: 2+1+4 => 8
S10: 8+1 => 16
S11: 1+8 => 16
S12: 1+1+2+2+8 => 16
S13: 1+1 => 2
S14: 1+1+4 => 8
S15: 1+1+1+1+1 => 5
S16: 5+1 => 6
S17: 8+5 => 16
S18: 8+5+5 => 24
S19: 8+5+5+1+2 => 24
S20: 8+5+5+2+1 => 24
Run Code Online (Sandbox Code Playgroud)

看看这个结果我无法理解用于顺序结构的布局(字段对齐和总大小)规则集CLR.有人能解释一下这种行为吗?

Cor*_*son 12

所有字段都根据其类型对齐.原生类型(int,byte等)都按其大小对齐.例如,a int将始终为4个字节的倍数,而一个字节可以是任何位置.

如果较小的字段位于a之前int,则必要时将添加填充以确保int正确对齐到4个字节.这就是为什么S5(1 + 1 + 4 = 8)和S8(1 + 2 + 4 = 8)将有填充并结束相同的大小:

[1][1][ ][ ][4] // S5
[1][ ][ 2  ][4] // S8
Run Code Online (Sandbox Code Playgroud)

另外,struct本身继承了其最对齐字段的对齐(即for S5S8,int是最对齐的字段,因此它们都具有4的对齐).对齐是这样继承的,这样当你有一个结构数组时,所有结构中的所有字段都将正确对齐.所以,4 + 2 = 8.

[4][2][ ][ ] // starts at 0
[4][2][ ][ ] // starts at 8
[4][2][ ][ ] // starts at 16
Run Code Online (Sandbox Code Playgroud)

请注意,4始终与4对齐.如果不继承最对齐的字段,则数组中的每个其他元素将int对齐6个字节而不是4个字节:

[4][2] // starts at 0
[4][2] // starts at 6 -- the [4] is not properly aligned!
[4][2] // starts at 12
Run Code Online (Sandbox Code Playgroud)

这将是非常糟糕的,因为并非所有体系结构都允许从未对齐的内存地址读取,甚至那些具有(可能非常大,如果在高速缓存行或页边界上)执行它的性能损失的内存地址.

除了基本性能之外,对齐也会在并发性方面发挥作用.C#内存模型保证最多4个字节宽的本机类型的读/写是原子的,而.NET具有像Interlocked类这样的原子特性.像这样的原子操作归结为CPU指令本身需要对齐的内存访问才能工作.

正确对齐非常重要!

您经常会看到聪明的本地编码人员在布置他们的结构时记住所有这些,将所有字段从大到小排序,以便将填充和结构大小保持在最低限度.