编译器为什么要将N字节数据类型对齐到N字节边界?

3

我不明白为什么编译器会将int对齐到4字节边界,short对齐到2字节边界,char对齐到1字节边界。 我知道如果处理器的数据总线宽度为4字节,则从非4字节倍数地址读取int需要2个内存读取周期。
那么,为什么编译器不将所有数据都对齐到4字节边界呢? 例如:

struct s {
 char c;
 short s;
};

这里,为什么编译器将short类型对齐到2字节边界?假设处理器可以在单个内存读取周期内获取4个字节,即使char和short之间没有任何填充,上述情况中读取short也只需要1个内存读取周期吗?

为什么编译器不将short类型对齐到4字节边界?


1
可能是内存对齐的目的的重复问题。 - user694733
结构填充对齐的目的是为了在一次机器读取中获取数据。在您的情况下,结构体将是4而不是8。您仍然可以通过使用掩码在一个周期内获取char或short。因此,在获取char时,处理器将获取4个字节并屏蔽掉24位。<br> 但是,如果您有这样的内容:<br> struct s { char c; int i}; 那么大小将变为8字节,因为您需要完整的4个字节才能在读取周期中获取整数。 - Nikhil Vidhani
@NikhilVidhani:我的问题不是关于填充的目的。我的问题是为什么在char和short之间填充字节而不是在short之后填充。假设处理器可以在单个周期内获取4个字节,无论填充发生在哪里,short都可以在1个周期内获取,对吧?那么,在上述情况下我们得到了什么样的节省呢?我猜这里有一些硬件层面的解释。 - linuxfreak
1
@linuxfreak 根据我的直觉...我认为如果short占用字节2和3,获取(屏蔽)最后16位比获取9-24位更容易。 - Nikhil Vidhani
@NikhilVidhani - 是的..我也这么认为。要获取位9-24,处理器除了掩码操作外还需要进行位移操作。 - linuxfreak
4个回答

4
这些对象必须适合数组。数组是连续的。因此,如果第一个元素是N字节对齐的,并且所有对象都是N字节大,则数组中所有对象必然也是N字节对齐的。
因此,如果“short”大小为2个字节,但4个字节对齐,则在数组中所有short之间将有2个字节的空洞,这是禁止的。
你会发现你的假设略有缺陷。我可以创建一个具有26个字符的结构体,它不会是26字节对齐的。它可以从任何地方开始。具有N字节类型的对齐方式等于N或除以N。

@iccthedral:绝对不是。一个26字节的类型可能会2字节对齐,但不会52字节对齐。 - MSalters
为什么short类型需要2字节对齐?无论在上述结构体中char和short之间是否添加填充字节,CPU只需要1个内存读取周期。那么,为什么在char和short之间要留出一个字节的填充呢?换句话说,上述结构体的对齐方式已经是4了,为什么不把short放在char下面而不留下填充字节呢? - linuxfreak
我认为它这样做是因为数据布局中的顺序很重要。而且它想要保持自然对齐。 - nullpotent
1
如果您使用4字节读取,那么short将被读入与其前面的char和填充相同的寄存器中。因此,您可能需要两个额外的指令来删除不需要的位。通过使用16位读取(通常可用),您只获取所需的16位,但是该16位读取必须对齐。 - MSalters
啊,好的,抱歉打扰了。只是想确保你的意思。 - nullpotent
显示剩余2条评论

2
首先,您的前提是错误的。每个对象都以某些基本对齐方式对齐。对于一些标量对象,对齐方式可能与对象的数据大小相同,但也可能更小或更大。例如,经典的32位架构(我在想i386)可能包括8字节双精度浮点数和10字节长双精度浮点数,两者均具有4字节对齐方式。请注意,上面我说的是“数据大小”,不要将其与“sizeof”混淆。
实际对象的大小可能大于数据大小,因为对象的大小必须是对象对齐的倍数。原因是对象的对齐始终相同,无论上下文如何。换句话说,对象的对齐仅取决于对象的类型。
因此,在结构中:
struct example1 {
  type1 a;
  type2 b;
};

struct example2 {
  type2 b;
  type1 a;
};

两个b的对齐方式相同。为了能够保证这种对齐方式,必须要求复合类型的对齐方式必须是成员类型的对齐方式的最大值。这意味着上面的struct example1struct example2具有相同的对齐方式。

对象的对齐方式仅取决于其类型的要求,这意味着类型的大小必须是其对齐方式的倍数。(任何类型都可以是数组的元素类型,包括仅有一个元素的数组。数组的大小是元素的大小和元素数量的乘积。因此,任何必要的填充必须是元素大小的一部分。)

通常,重新排列复合类型中的成员可能会改变复合类型的大小,但不能改变复合类型的对齐方式。例如,下面的两个结构体具有相同的对齐方式——即double的对齐方式——但第一个结构体几乎肯定更小:

struct compact {
  double d;   // Must be at offset 0
  char   c1;  // Will be at offset sizeof(double)
  char   c2;  // Will be at offset sizeof(double)+sizeof(char).
};

struct bloated {
  char   c1;  // Must be at offset 0
  double d;   // Will be at offset alignof(double)
  char   c2;  // Will be at offset (alignof(double) + sizeof(double))
};

我认为这并没有回答我的问题。我在询问有关填充字节的硬件级别解释。在short的情况下,填充字节是添加在char和short之间的。为什么不在short之后添加填充字节呢?这两种情况都只需要进行1个内存周期读取,对吧?(假设处理器可以在1个周期内提取4个字节)。 - linuxfreak
@linuxfreak:在这种情况下可能没有硬件需求。但是有软件需求,这就是我要解释的内容。C要求类型的对齐方式保持一致,如果有时短整型必须2字节对齐(例如,以便不跨越缓存行),那么它始终必须是2字节对齐。如果您没有从答案中理解这一点,请告诉我,我会尝试修复它。 - rici

0

我想我找到了我的问题的答案。可能有两个原因导致字节在char和short之间填充而不是在short之后填充。

1)某些体系结构可能具有仅从内存中提取2个字节的2个字节指令。如果是这种情况,则需要2个内存读取周期才能获取short。

2)某些体系结构可能没有2字节指令。即使在这种情况下,处理器也会从内存中提取4个字节到寄存器,并屏蔽未使用的字节以获取short值。如果字节在char和short之间没有填充,则处理器必须移动字节以获取short值。

上述两种情况都可能导致性能较慢。这就是为什么byte short是2字节对齐的原因。


1
你忘记了一个重要案例:存在某些架构甚至不允许读取未对齐的值。在这种情况下,编译器将不得不发出两个内存加载而不是一个加上一些位操作指令来获取该值,并且在写入时也必须执行类似的操作。后者甚至更为棘手,因为程序不能允许发明对相邻数据的写入(由于多线程),因此编译器将不得不回退到逐字节写入该值。显然,禁止语言中存在这些陷阱是更好的选择。 - cmaster - reinstate monica

0

编译器按照目标处理器(微)架构ABI规定对齐数据。例如,可以查看x86-64 ABI规范

如果您的编译器与某些ABI规范不同,则无法调用遵守该ABI的库中的函数!

在您的示例中,如果(在x86-64上)短字段s未对齐到2个字节,则处理器将不得不更多地工作(可能发出两个访问)才能获取该字段。

此外,在许多x86-64芯片上,缓存行通常是16(或更少)字节的倍数。因此,将调用堆栈帧对齐到16字节是有意义的。这对于类似向量的局部变量(AVX、SSE3等)是必需的。

在某些处理器上,数据对齐不良可能会导致故障(例如机器异常的中断)或显著减慢处理速度。此外,它可能会使一些访问非原子化(用于多核处理)。因此,一些ABI规定了比严格必要更多的ABI。此外,一些CPU的最新功能(如矢量化,例如通过SIMD指令如AVX或SSE3)确实受益于非常对齐的数据(例如对齐到16字节)。如果编译器知道这样的强制对齐,它可能会更多地进行优化以使用这些指令。

在所有基于Core2的处理器上,缓存行是64字节而不是16字节。调用堆栈在64位代码中对齐到16字节。对于32位代码,它不一定对齐到16字节。 - Z boson
抱歉...我在“16字节的倍数”中漏掉了“多个”这个词。对于core2来说,这个倍数是4。也许在Pentium4上是2(32字节)?在某些时候它只有16字节吗(例如,在Pentium4之前)?这是调用堆栈是16字节的倍数的真正原因吗? - Z boson

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