struct实例字段的内存布局(Layout)和大小

背景

在C/C++中,struct类型中的成员的一旦声明,则实例中成员在内存中的布局 (Layout) 顺序就定下来了,即与成员声明的顺序相同,并且在默认情况下总是按照结构中占用空间最大的成员进行对齐(Align);当然我们也可以通过设置或编码来设置内存对齐的方式。

然而在 .net 托管环境中,CLR 提供了更自由的方式来控制 struct 中 Layout:我们可以在定义 struct时,在 struct 上运用 StructLayoutAttribute 特性来控制成员的内存布局。默认情况下,struct 实例中的字段在栈上的布局 (Layout) 顺序与声明中的顺序相同,即在 struct 上运用 [StructLayoutAttribute(LayoutKind.Sequential)]特性,这样做的原因是结构常用于和非托管代码交互的情形。但是对于引用类型,默认的则是 LayoutKind.Auto

如果我们的值类型不会与非托管代码互操作,就应该覆盖 C# 编译器的默认设定。 LayoutKind 除了 Sequential 成员之外,还有两个成员 AutoExplicit,给 StructLayoutAttribute 传入 LayoutKind.Auto 可以让 CLR 按照自己选择的最优方式来排列实例中的字段;传入 LayoutKind.Explicit 可以使字段按照我们的在字段上设定的 FieldOffset 来更灵活的设置字段排序方式,但这种方式也挺危险的,如果设置错误后果将会比较严重。

Struct 的内存布局

struct 的最小大小为 1 Byte:

struct EmptyStruct {};
// sizeof (EmptyStruct) = 1

内存布局流程

一个 struct 的内存布局流程可以简化为下面几步:

  • struct 放到地址0上
  • struct 的所有成员顺序地依次放置到自己的偏移位置上
  • 所有成员放置完毕,对struct的内容大小(最后一个成员的终了位置)进行对齐,计算出新大小

流程中涉及到两点:

  • 如何确定一个成员的放置起始位置
  • 如何最后对struct的内容大小进行对齐

成员的起始位置

当放置完上一个成员之后,对接下来的成员放置位置是有要求的,要求这个位置偏移(对于struct的起始位置)必须是这个成员的内部最大类型的大小的倍数。

在上个放置成员尾部和当前成员起始位置之间如果有间隙,那么编译器会填充无效字节。

然后就可以把当前成员放置进去,放入的大小为当前成员的总大小。

struct 的大小对齐

成员都放置完毕之后,struct的内容大小有个规则,必须是内部最大类型的大小的倍数。没错,就是上面那个 maxInnerUnitSize。

然后在最后一个成员末尾到struct的新大小末尾会填充字节(如果有空隙的话)。

案例

下面就看几个示例,算下四个struct各占多少Byte ?

1. [StructLayout(LayoutKind.Sequential)]

struct StructDeft//C#编译器会自动在上面运用[StructLayout(LayoutKind.Sequential)]
{
 bool i;  //1Byte
 double c;//8Byte
 bool b;  //1Byte
}

sizeof(StructDeft) 得到的结果是 24 Byte!啊哈,本身只有 10 Byte 的数据却占有了 24 Byte 的内存,这是因为默认(LayoutKind.Sequential)情况下,CLR 对 struct 的 Layout 的处理方法与 C/C++ 中默认的处理方式相同(8+8+8=24),即按照结构中占用空间最大的成员进行对齐(Align)。10 Byte 的数据却占有了 24 Byte,严重地浪费了内存,所以如果我们正在创建一个与非托管代码没有任何互操作的 struct类型,最好还是不要使用默认的 StructLayoutAttribute(LayoutKind.Sequential) 特性。

2. [StructLayout(LayoutKind.Explicit)]

[StructLayout(LayoutKind.Explicit)]
struct BadStruct
{
    [FieldOffset(0)]
    public bool i;  //1Byte
    
    [FieldOffset(0)]
    public double c;//8byte
    
    [FieldOffset(0)]
    public bool b;  //1Byte
}

sizeof(BadStruct) 得到的结果是 8 Byte,得出的基数 8 显示 CLR 并没对结构体进行任何内存对齐(Align);本身要占有 10Byte 的数据却只占了 8Byte,显然有些数据被丢失了,这也正是我给 struct 取BadStruct 作为名字的原因。如果在 struct 上运用了 [StructLayout(LayoutKind.Explicit)],计算 FieldOffset 一定要小心,例如我们使用上面 BadStruct 来进行下面的测试:

StructExpt e = new StructExpt();
e.c = 0;
e.i = true;
Console.WriteLine(e.c);

输出的结果不再是 0 了,而是 4.94065645841247E-324 ,这是因为 e.ce.i 共享同一个 byte,执行e.i = true;时也改变了 e.c,CPU在按照浮点数的格式解析 e.c 时就得到了这个结果。所以在运用 LayoutKind.Explicit 时千万别吧 FieldOffset 算错了:)

3. [StructLayout(LayoutKind.Auto)]

sizeof(StructAuto) 得到的结果是12 Byte。下面来测试下这 StructAuto 的三个字段是如何摆放的:

unsafe
{
      StructAuto s = new StructAuto();
      Console.WriteLine(string.Format("i:{0}", (int)&(s.i)));
      Console.WriteLine(string.Format("c:{0}", (int)&(s.c)));
      Console.WriteLine(string.Format("b:{0}", (int)&(s.b)));
}

// 测试结果:
i:1242180
c:1242172
b:1242181

即CLR会对结构体中的字段顺序进行调整,将 i 调到 c 之后,使得 StructAuto 的实例 s 占有尽可能少的内存,并进行 4byte 的内存对齐(Align),字段顺序调整结果如下图所示:
在这里插入图片描述

结论

  • 默认(LayoutKind.Sequential)情况下,CLR 对 struct 的 Layout 的处理方法与 C/C++ 中默认的处理方式相同,即按照结构中占用空间最大的成员进行对齐(Align);
  • 使用 LayoutKind.Explicit 的情况下,CLR 不对结构体进行任何内存对齐(Align),而且我们要小心就是FieldOffset
  • 使用 LayoutKind.Auto 的情况下,CLR 会对结构体中的字段顺序进行调整,使实例占有尽可能少的内存,并进行 4 Byte 的内存对齐(Align)。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值