我正在阅读《游戏编码完整版》第4版。有一个关于内存对齐的主题。在下面的代码中,作者说,第一个结构确实很慢,因为它既没有位对齐也没有字节对齐。第二个不是位对齐而是字节对齐。最后一个很快,因为两者都有。他说,没有编译指示,编译器将对齐内存本身,这会浪费内存。我无法真正获得计算结果。
这是文本的一部分:
如果让编译器通过添加未使用的字节来优化SlowStruct,则每个结构将是24个字节而不是14个字节。在第一个char变量之后填充七个额外的字节,并在末尾添加其余字节。这样可以确保整个结构始终以8字节为边界开始。这大约是浪费空间的40%,这都是由于成员变量的粗心排序所致。
这是粗体字的结论行:-
不要让编译器浪费宝贵的内存空间。让您的一些 脑细胞发挥作用,并对齐您自己的成员变量。
请让我看一下计算并更清楚地解释填充概念。
码:-
#pragma pack(push, 1) struct ReallySlowStruct { char c : 6; __int64 d : 64; int b : 32; char a : 8; }; struct SlowStruct { char c; __int64 d; int b; char a; }; struct FastStruct { __int64 d; __int b; char a; char c; char unused[2]; }; #pragma pack(pop)
jwsc.. 6
本书中给出的示例高度依赖于所使用的编译器和计算机体系结构。如果在自己的程序中测试它们,则可能会得到与作者完全不同的结果。我将假定使用64位体系结构,因为作者也会这样做,因为我在描述中已经读过。让我们一一查看示例:
如果所使用的编译器支持非字节对齐的结构成员,则ReallySlowStruct,“ d”的开头将在结构的第一个字节的第七位。听起来非常节省内存。问题是,C不允许位处理。因此,要将newValue保存到“ d”成员,编译器必须执行很多移位操作:将“ newValue”的前两位保存在byte0中,向右移6位。然后将“ newValue”向左移两位,并从字节1开始保存。字节1是未对齐的存储器位置,这意味着大容量存储器传输指令将不起作用,编译器必须一次保存每个字节。
SlowStruct 变得更好。编译器可以摆脱所有的麻烦。但是,写入“ d”仍然需要一次写入每个字节,因为它与本机“ int”的大小不匹配。在64位系统上,本机大小为8。因此,每个不可被8整除的内存地址一次只能访问一个字节。更糟糕的是,如果我关闭打包,则会浪费大量的内存空间:每个成员后面跟一个int都将填充足够的字节,以使整数从8的可除内存位置开始。在这种情况下:char a和c都将占用8个字节。
FastStruct, 它与目标计算机上int的大小对齐。“ b”占用了8个字节。因为所有字符都捆绑在一个位置,所以编译器不会填充它们,也不会浪费空间。每个字符只有1个字节,因此我们不需要填充它们。完整的结构总计为16个字节。除以8,因此无需填充。
本书中给出的示例高度依赖于所使用的编译器和计算机体系结构。如果在自己的程序中测试它们,则可能会得到与作者完全不同的结果。我将假定使用64位体系结构,因为作者也会这样做,因为我在描述中已经读过。让我们一一查看示例:
如果所使用的编译器支持非字节对齐的结构成员,则ReallySlowStruct,“ d”的开头将在结构的第一个字节的第七位。听起来非常节省内存。问题是,C不允许位处理。因此,要将newValue保存到“ d”成员,编译器必须执行很多移位操作:将“ newValue”的前两位保存在byte0中,向右移6位。然后将“ newValue”向左移两位,并从字节1开始保存。字节1是未对齐的存储器位置,这意味着大容量存储器传输指令将不起作用,编译器必须一次保存每个字节。
SlowStruct 变得更好。编译器可以摆脱所有的麻烦。但是,写入“ d”仍然需要一次写入每个字节,因为它与本机“ int”的大小不匹配。在64位系统上,本机大小为8。因此,每个不可被8整除的内存地址一次只能访问一个字节。更糟糕的是,如果我关闭打包,则会浪费大量的内存空间:每个成员后面跟一个int都将填充足够的字节,以使整数从8的可除内存位置开始。在这种情况下:char a和c都将占用8个字节。
FastStruct, 它与目标计算机上int的大小对齐。“ b”占用了8个字节。因为所有字符都捆绑在一个位置,所以编译器不会填充它们,也不会浪费空间。每个字符只有1个字节,因此我们不需要填充它们。完整的结构总计为16个字节。除以8,因此无需填充。