什么是字节对齐?
现代计算机中内存空间都是按照 byte 划分的,从理论上讲似乎对任何类型的变量的访问可以从任何地址开始,但实际情况是在访问特定类型变量的时候经常在特定的内存地址访问,这就需要各种类型数据按照一定的规则在空间上排列,而不是顺序的一个接一个的排放,这就是对齐。
为什么要字节对齐?
各个硬件平台对存储空间的处理上有很大的不同。一些平台对某些特定类型的数据只能从某些特定地址开始存取。比如有些架构的CPU在访问一个没有进行对齐的变量的时候会发生错误,那么在这种架构下编程必须保证字节对齐。
最常见的是如果不按照适合其平台要求对数据存放进行对齐,会在存取效率上带来损失。比如有些平台每次读都是从偶地址开始,如果一个 int 型(假设为32位系统)存放在偶地址开始的地方,那么一个读周期就可以读出这 32bit 的数据,而如果存放在奇地址开始的地方,就需要2个读周期,并对两次读出的结果的高低字节进行拼凑才能得到该 32bit 数据。显然在读取效率上下降很多。
什么是字节对齐规则?
基础概念
- 指定对齐值:编译器或者程序员指定的对齐值,packLen。
- 默认对齐值:结构体中每一个数据成员及结构体本身都有默认对齐值,记为 defaultLen。
- 成员偏移量:即相对结构体起始位置的长度,记为 offset。
- 成员长度:结构体中每个数据成员的长度(注意为结构体成员补齐之后的长度),记为memberLen。
- 有效对齐值:validLen = min(packLen,defaultLen)。
字节对齐规则
- 对齐规则:offset % validLen=0,其中 validLen = min(packLen,defaultLen)。
- 填充规则:如成员变量遵守对齐规则,则不需要对齐补齐;在其前面填充一些字节保证该成员对齐。需填充的字节数记为pad。
- 结构体的总大小,也就是 sizeof 的结果,必须是其内部成员最大有效对齐值的整数倍,不足的要补齐。
怎么指定对齐值?
StructLayoutAttribute.Pack 字段 (System.Runtime.InteropServices) | Microsoft Learn
[StructLayout(LayoutKind.Sequential, Pack = 0)]
//LayoutKind:
//Sequential,对象的成员按照它们在被导出到非托管内存时出现的顺序依次布局。
//这些成员根据在 Pack 中指定的封装进行布局,并且可以是不连续的。
//Explicit,在未管理内存中的每一个对象成员的精确位置是被显式控制的,服从于 Pack 字段的设置。
//每个成员必须使用 FieldOffsetAttribute 指示该字段在类型中的位置。
//[StructLayout(LayoutKind.Explicit)]
//public struct Rect
//{
// [FieldOffset(0)] public int left;
// [FieldOffset(4)] public int top;
// [FieldOffset(8)] public int right;
// [FieldOffset(12)] public int bottom;
//}
//Auto,运行库自动为非托管内存中的对象的成员选择适当的布局。
//使用此枚举成员定义的对象不能在托管代码的外部公开。尝试这样做将引发异常。
//Pack:指定对齐值,1、2、4、8、16、32、64、128或特殊值0,0表示当前操作平台默认的压缩大小。
怎么计算struct占用多少个字节
平台:32bit
结构体本身的默认对齐值为其变量中最大对齐值
示例一:不指定对齐值
struct ExampleStruct
{
public byte b1;
public byte b2;
public int i3;
}
// Size: 8
// b1 Offset: 0
// b2 Offset: 1
// i3 Offset: 4
假设从地址空间0x0000开始排放。没有指定对齐值,默认为4字节。
- 变量 b1
- 本身的默认对齐值为1,有效对齐值为1,成员偏移量为0
- 存放地址 0x0000
- 变量 b2
- 基本同上,成员偏移量为1
- 存放地址 0x0001
- 变量 i3
- 本身的默认对齐值为4,有效对齐值为4
- (2 + 2) % 4 = 0,需要填充2个字节,偏移量为4
- 存放地址:0x0004 ~ 0x0007
- 0x0000 ~ 0x0007,长度为8
- 结构体本身的默认对齐值为4
- 结构体的有效对齐值为4 = min(4, 4)
- 8 % 4 = 0,所以结构体不需要填充,总长度为8。
示例二:指定对齐值
[StructLayout(LayoutKind.Sequential, Pack = 2)]
struct ExampleStruct2
{
public byte b1;
public byte b2;
public int i3;
}
// Size: 6
// b1 Offset: 0
// b2 Offset: 1
// i3 Offset: 2
- 变量 b1
- 本身的默认对齐值为1,有效对齐值为1,成员偏移量为0
- 存放地址:0x0000
- 变量 b2
- 基本同上,成员偏移量为1
- 存放地址:0x0001
- 变量 i3
- 本身的默认对齐值为4,有效对齐值为2
- 2 % 2 = 0,偏移量为2,
- 存放地址:0x0002 ~ 0x0005
- 0x0000 ~ 0x0005,长度为6
- 结构体本身的默认对齐值为4
- 结构体的有效对齐值也是2 = min(2, 4)
- 6 % 2 = 0,所以结构体不需要填充,总长度为6。
示例三:尽量把默认对齐值小的成员放在前面定义
struct ExampleStruct3
{
public byte b1;
public int i3;
public byte b2;
}
// Size: 12
// b1 Offset: 0
// b2 Offset: 4
// i3 Offset: 8
- 变量 b1
- 本身的默认对齐值为1,有效对齐值为1,偏移量为0
- 存放地址 0x0000
- 变量 i3
- 本身的默认对齐值为4,有效对齐值为4
- (1 + 3) % 4 = 0,需要填充3个字节,偏移量为4
- 存放地址 则为 0x0004 ~ 0x0007
- 变量 b2
- 成员偏移量为8,满足 8 % 4 = 0
- 存放地址为 0x0008
- 0x0000 ~ 0x0008,长度为9
- 结构体本身的默认对齐值为4
- 结构体的有效对齐值也是4
- (9 + 3) % 4 = 0,所以结构体需要填充3个字节,总长度为12。