【编程】C#中的结构体字节对齐

【编程】C#中的结构体字节对齐

基本原则

  1. 结构体变量的首地址能够被其对齐字节数大小所整除
  2. 成员相对结构体首地址的偏移是【当前成员对齐数】的整数倍,不满足对前一成员填充字节
  3. 结构体总大小为【最大成员对齐数】的整数倍

成员对齐数,以下三者取最小值:

  1. 类型的非托管字节数
  2. Pack值
  3. 当前环境默认对齐字节数,例如Window 64位环境下默认是8字节

从实例看注意事项

例一
    [StructLayout(LayoutKind.Sequential, Pack = 2)]
    class Foo
    {
        // 整型            成员大小    计算下一成员首地址    下一成员对齐数    下一成员真实首地址 
        public sbyte i1;   // 1        +0  = 1               [1]               +0 = 1 
        public byte i2;    // 1        +1  = 2               2                 +0 = 2   
        public short i3;   // 2        +2  = 4               2                 +0 = 4
        public ushort i4;  // 2        +4  = 6               2                 +0 = 6
        public int i5;     // 4        +6  = 10              2                 +0 = 10
        public uint i6;    // 4        +10 = 14              2                 +0 = 14
        public long i7;    // 8        +14 = 22              2                 +0 = 22
        public ulong i8;   // 8        +22 = 30              2                 +0 = 30
        public nint i9;    // [8]      +30 = 38              2                 +0 = 38
        public nuint i10;  // [8]      +38 = 46              2                 +0 = 46
        // 浮点型
        public float f1;   // 4        +46 = 50              2                 +0 = 50
        public double f2;  // 8        +50 = 58              2                 +0 = 58
        public decimal f3; // 16       +58 = 74              2                 +0 = 74
        // 其他
        public bool b;     // 4[1]     +74 = 78              2                 +0 = 78
        public char c;     // 1[2]     +78 = 79              [2]               +1 = 80
    }                                                        //最大成员对齐数       结构体的非托管字节数

首先看 i2 的对齐数,这个结构体定义 Pack 值为 2,byte 类型占 1 字节,我的环境是 Windows 64 默认对齐字节数是 8,三者取最小,i2 的对齐数应该 1。因此第一行中 i1 是不需要对齐的。

之后看 i9 和 i10 的对齐数,这两个类型的非托管字节数和环境有关,我的环境中是 8 字节,然而结构体开头就定义了 Pack 值为 2,取最小值仍旧会取到 2。i8 占位后,下一成员首地址为 30,能够被 2 整除。因此这一行也不需要对齐。

最后看一下 bool 和 char,这是两个很特殊的存在。如果使用 sizeof 求 bool 的字节数为 1,求 char 的字节数为 2,但是使用 Marshal.SizeOf 求 bool 的非托管字节数为 4,求 char 的非托管字节数为 1。计算字节对齐要聚焦非托管字节数,即 bool 取 4,char 取 1。

例二
    struct Bar
    {
        // 整型
        public sbyte i1;   // 1        +0  = 1               1                 +0  = 1
        public byte i2;    // 1        +1  = 2               2                 +0  = 2
        public short i3;   // 2        +2  = 4               2                 +0  = 4
        public ushort i4;  // 2        +4  = 6               4                 +2  = 8
        public int i5;     // 4        +8  = 12              4                 +0  = 12
        public uint i6;    // 4        +12 = 16              8                 +0  = 16    
        public long i7;    // 8        +16 = 24              8                 +0  = 24
        public ulong i8;   // 8        +24 = 32              8                 +0  = 32
        public nint i9;    // 8        +32 = 40              8                 +0  = 40
        public nuint i10;  // 8        +40 = 48              4                 +0  = 48
        // 浮点型
        public float f1;   // 4        +48 = 52              8                 +4  = 56
        public double f2;  // 8        +56 = 64              [8]               +0  = 64
        public decimal f3; // 16       +64 = 80              4                 +0  = 80
        // 其他
        public bool b;     // 4[1]     +80 = 84              1                 +0  = 84
        public char c;     // 1[2]     +84 = 85              [8]               +3  = 88  
    }

第二个例子没有用类来定义结构体类型,也就没有 Pack 值这一说,那么大多数情况下,成员的对齐字节数就是成员的非托管字节数了,不过也要注意此时选取对齐数还是需要二者取最小的,因为环境默认对齐数是始终要考虑的。

我们可以将目光聚焦这个例子中的 f3,decimal 占用16字节,但是环境默认对齐数是 8,二者取小为 8,并不是 decimal 的非托管字节数。

然后我们看一下最后一行,结构体的最后一个成员没有下一个成员,但是仍旧有需要对齐的可能,原因可以回顾基本原则的第三条。这里结构中最大成员对齐数是 8,c 变量占位后,下一成员首地址为 85,并不是 8 的倍数,因此结构体的总字节数需要补齐到 88。

例三
    struct Qux
    {
        public decimal quux; // 16    +0  = 16    1      +0 = 16 
        public byte quuux;   // 1     +16 = 17    [8]    +7 = 24
    }

注意结构体中最大成员对齐数包括第一个成员,例如本例中第一个成员的对齐数是 8,所以结构体最后要补 7 位,使结构体大小等于 8 的倍数。

例四
    [StructLayout(LayoutKind.Sequential, Pack = 16)]
    class FooBar
    {
        public byte foo1;    // 1     +0  = 1     [8]    +7  = 8     
        public decimal foo2; // 16    +8  = 24    8      +0  = 24   
        public double foo3;  // 8     +24 = 32    [8]    +0  = 32
        public decimal foo4; // 16    +32 = 48    4      +0  = 48
        public int foo5;     // 4     +48 = 52    [8]    +4  = 56
    }

如果不考虑环境的默认对齐字节数,本例十分容易出错。这里 Pack 的 16 在 Windows 64 下永远取不到,因为 环境默认参数为 8 字节,始终是更小的值。

例五
    [StructLayout(LayoutKind.Sequential)]
    class BarFoo
    {
        public bool bar1;    // 4     +0  = 4     1      +0  = 4     
        public char bar2;    // 1     +4  = 5     4      +3  = 8   
        public bool bar3;    // 4     +8  = 12    1      +0  = 12
        public char bar4;    // 1     +12 = 13    4      +3  = 16
        public int bar5;     // 4     +16 = 20    1      +0  = 20
        public char bar6;    // 1     +20 = 21    4      +3  = 24
        public bool bar7;    // 4     +24 = 28    4      +0  = 28
    }

这个例子如果把 bool 字节数视为 1,把 char 字节数视为 2,就会一直迷惑一直错了。

验证函数
    private void Start()
    {
        Debug.Log("Class Foo:" + Marshal.SizeOf<Foo>());
        Debug.Log("Struct Bar:" + Marshal.SizeOf<Bar>());
        Debug.Log("Qux:" + Marshal.SizeOf<Qux>());
        Debug.Log("FooBar: " + Marshal.SizeOf<FooBar>());
        Debug.Log("BarFoo: " + Marshal.SizeOf<BarFoo>());
        Debug.Log("sbyte:" + sizeof(sbyte) + " | " + "byte:" + sizeof(byte) + " | " + "short:" + sizeof(short) + " | " + "ushort:" + sizeof(ushort) 
            + " | " + "int:" + sizeof(int) + " | " + "unit:" + sizeof(uint) + " | " + "long:" + sizeof(long) + " | " + "ulong:" + sizeof(ulong) 
            + " | " + "float:" + sizeof(float) + " | " + "double:" + sizeof(double) + " | " + "decimal:" + sizeof(decimal) 
            + " | " + "bool:" + sizeof(bool) + " | " + "char:" + sizeof(char));
        Debug.Log("nint:" + Marshal.SizeOf<nint>());
        Debug.Log("nuint:" + Marshal.SizeOf<nuint>());
        Debug.Log("sbyte:" + Marshal.SizeOf<sbyte>() + " | " + "byte:" + Marshal.SizeOf<byte>() + " | " + "short:" + Marshal.SizeOf<short>() + " | " + "ushort:" + Marshal.SizeOf<ushort>()
            + " | " + "int:" + Marshal.SizeOf<int>() + " | " + "unit:" + Marshal.SizeOf<uint>() + " | " + "long:" + Marshal.SizeOf<long>() + " | " + "ulong:" + Marshal.SizeOf<ulong>()
            + " | " + "float:" + Marshal.SizeOf<float>() + " | " + "double:" + Marshal.SizeOf<double>() + " | " + "decimal:" + Marshal.SizeOf<decimal>()
            + " | " + "bool:" + Marshal.SizeOf<bool>() + " | " + "char:" + Marshal.SizeOf<char>());
    }

运行结果:
验证
因为Unity比较熟练就直接在Unity里验证了,类似的验证方法在其他C#环境中也很容易实现。

碎碎念

事实上单独理解同一种环境下同一种语言的字节对齐没太大意义,我意思是针对我这种不怎么接触硬件相关的程序员而言。我遇到的问题经常是同一种结构在 C++ 和 C# 工程之间传递时成员变量数值对应不上,或者从服务端拿到的结构数据解析异常。后续有时间也会补一下 C++ 的字节对齐相关,这种东西还是记录一下比较好,同一个 bool 在不同语言不同环境下一会儿字节数是 1 一会儿是 4,真的很容易记错。

  • 10
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值