C/C++中的内存对齐问题

26
我正在阅读《Game Coding Complete 4th edition》。有一个关于内存对齐的话题。在下面的代码中,作者说第一个结构体非常慢,因为它既不是按位对齐也不是按字节对齐。第二个结构不是按位对齐,但是按字节对齐。最后一个结构很快,因为它既按位对齐又按字节对齐。他说如果没有使用#pragma,编译器将自行对齐内存,这会导致浪费内存。我无法真正理解计算方法。
这是文本的一部分:-
如果让编译器通过添加未使用的字节来优化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)

5
它既不是按位对齐也不是按字节对齐。按位对齐?我不理解,C中的最小可寻址地址是一个字节,对吗? - David Ranieri
2
在C中,结构体填充是指编译器插入额外的字节以使结构体成员对齐。这些填充字节不包含任何有用的数据,仅仅是为了对齐目的而存在。填充可以影响结构体的大小和内存布局。 - NathanOliver
1
还可以参考这个维基页面: https://en.wikipedia.org/wiki/Data_structure_alignment - NathanOliver
2
什么是“位边界”?在位边界之间会发生什么? - Kerrek SB
作者使用编译器扩展来告诉编译器通过防止填充字节来生成不良对齐的结构,然后手动插入填充字节。我希望这只是为了告诉您有关填充和对齐的知识,并且这不是作者实际要做的事情。您应该将其视为此类信息。 - nwp
显示剩余9条评论
1个回答

40

这本书中的例子高度依赖于所用的编译器和计算机体系结构。如果您在自己的程序中测试它们,可能会得到与作者完全不同的结果。我将假设64位架构,因为作者也是这样做的,从我在描述中读到的。

让我们逐个看一下这些例子:

ReallySlowStruct如果使用的编译器支持非字节对齐结构成员,则“d”的起始位置将位于结构的第一个字节的第七位。对于节省内存来说听起来很不错。但问题是,C语言不允许按位寻址。因此,为了将newValue保存到“d”成员中,编译器必须执行大量的位移操作:将“newValue”的前两位保存在byte0中,向右移动6位。然后将“newValue”左移两位并从byte 1开始保存它。Byte 1是非对齐的存储位置,这意味着批量内存传输指令无法工作,编译器必须一次保存每个字节。

SlowStruct情况有所改善。编译器可以摆脱所有位操作。但是,写入“d”仍需要一次一个字节地写入,因为它不对齐到本机“int”大小。64位系统上的本机大小为8。因此,每个不能被8整除的内存地址只能一次访问一个字节。更糟糕的是,如果我关闭打包,则会浪费大量的内存空间:每个紧随整数后面的成员都将使用足够的字节进行填充,以使整数从可被8整除的内存位置开始。在这种情况下:char a和c都将占用8个字节。

FastStruct 它对齐到目标机器上int的大小。正如它应该占用8个字节的“d”。因为所有字符都集中在一起,编译器不会对它们进行填充并浪费空间。字符只有1个字节,所以我们不需要对它们进行填充。完整结构的总大小为16个字节。可被8整除,因此不需要填充。


在大多数情况下,您无需关心对齐问题,因为默认对齐已经是最优的。但是,在某些情况下,通过为数据结构指定自定义对齐方式,可以实现显著的性能提升或节省内存。

在内存空间方面,编译器以一种自然对齐结构的方式进行填充每个元素。

struct x_
{
   char a;     // 1 byte
   int b;      // 4 bytes
   short c;    // 2 bytes
   char d;     // 1 byte
} bar[3];

struct x_ 被编译器填充,因此变成:

// Shows the actual memory layout
struct x_
{
   char a;           // 1 byte
   char _pad0[3];    // padding to put 'b' on 4-byte boundary
   int b;            // 4 bytes
   short c;          // 2 bytes
   char d;           // 1 byte
   char _pad1[1];    // padding to make sizeof(x_) multiple of 4
} bar[3];

来源: https://learn.microsoft.com/zh-cn/cpp/cpp/alignment-cpp-declarations?view=vs-2019


1
谢谢您回答我的问题。刚才我从不同的链接中运行了一些代码,以及书中给出的代码,并进行了实验,现在我清楚地知道没有特定的填充标准。但是作者暗示为了提高性能,要了解编译器的填充规则并以适当的方式排序声明。您的答案也非常有帮助。谢谢。 - Sourabh Mittal
我在FastStruct代码中犯了一个错误。我忘记添加64个int变量。非常抱歉,请相应地更新答案。 - Sourabh Mittal
谢谢您纠正我的答案。我非常感激您的帮助,现在我对这个主题有了更深刻的理解。再次感谢。 - Sourabh Mittal
3
在你对FastStruct的解释中,你提到“b”占用了8个字节。这应该是“d”吧? - Victor Stone

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接