为什么64位机器上的结构体对齐到4字节(32位)?

3

我尝试通过这段代码了解有关结构填充的一些内容:

#include <stdio.h>
#include <stdint.h>

struct azaza { // of course suboptimal arrangement of elements
     uint32_t addr1;
     uint32_t addr2;
     uint8_t tmp;
     uint32_t addr3;
     uint8_t flags;
};

int main(void) {
     printf("%d\n", sizeof(struct azaza));

     return 0;
}

输出结果为:20
但我期望的是24,因为我的机器和操作系统都是64位的,我认为对齐应该在4字节边界上。 为什么在x86-64操作系统上结构体的对齐方式是在4字节边界上?

因为您没有任何需要64位对齐的成员。 - undefined
2
如果在结构体中只有一个uint8_t成员,你会更加惊讶的... - undefined
@IłyaBursov 使用-m64参数得到相同的结果。 - undefined
@EugeneSh. 如果你带来了信息到课堂上,我希望你能和大家分享。 - undefined
1
@TimRandall 我已经提出了一个涵盖所有内容的重复建议。 - undefined
显示剩余6条评论
3个回答

2
“64位机器”这个术语是含糊的。计算机处理器和系统有几个特性,它们在同一台机器上可能具有不同的大小,包括:
- 处理器寄存器的宽度。 - 地址的宽度。 - 数据总线的宽度。 - 算术逻辑单元的宽度。
暂时假设所有这些都是64位。即使如此,为什么我们要求例如uint32_t对齐到64位呢?
要求对齐的一个原因是为了避免在内存传输中分割访问。如果总线宽度为64位,则系统通常设计为以8个字节(64位)的倍数访问内存。当处理器想要读取某些内存,例如从64位地址读取时,它只发送前61位到内存设备。 (61很多,但我们已经假设这台机器上的所有东西都是64位)。 内存设备获取与这61位匹配的所有八个字节,即我们未发送的低三位的八个组合。它每次获取八个字节,因为这是适合总线的,并且我们希望效率高。
因此,每当进程从内存中读取时,它总是会获取八个字节,并且这些字节将是64位对齐的。
现在我们可以看到,如果uint32_t从某个地址开始,例如xxx0101,其中x表示我们不关心的位,那么它的四个字节将在地址xxx0101、xxx0110、xxx0111和xxx1000。但是第四个字节在八个字节中的另一组。前三个都在同一组中,由初始位xxx0寻址的组。最后一个字节在新组xxx1中。为了读取这个uint32_t,我们必须从内存中进行两次读取。这是低效的。
但是,如果uint32_t位于地址xxx0000或xxx1000中,则其字节都在一个组内。它们可能是该组中的前四个或后四个字节,因此我们需要处理器能够从内存中选择前四个或后四个字节,但只需要从内存中读取一次即可获取字节。
因此,对于uint32_t,四字节对齐就足以确保它对齐得足够好,我们只需要从内存中读取一次即可获取它。
很少需要要求8字节对齐。一个原因是,如果它是8字节对齐的,我们就不需要处理器中额外的电线和开关来选择8个字节中的前4个或后4个字节。我们只需要取前4个。但这种微小的优势远远被这样一个事实所淹没:这意味着我们每8个字节只能存储一个uint32_t。一半的内存将被浪费用于填充。使用4字节对齐,我们可以很好地读取uint32_t对象,并且可以同时读取两个。
对于uint8_t,8字节对齐甚至更糟糕,我们每8个字节只能有一个uint8_t,浪费了87.5%的内存。
大多数情况下,长度为n字节的对象只需要具有n字节对齐才能与硬件良好配合(假设n是2的幂)。该对齐方式将使它们完美地适应总线和内存操作,无论它们的宽度如何。
此外,如果总线宽度为b,对象大小为n,则对齐要求可能只是bn中较小的一个。一旦一个对象大于总线宽度,我们将需要多次传输才能获取它,并且通常不需要比总线宽度更高的对齐方式。

1
非常好的解释,谢谢!所以对齐的主要原因基本上是为了提高内存总线操作的效率(避免分割访问)?还有其他一些原因吗?顺便问一下,我如何检查内存总线宽度? - undefined
1
这有点复杂。按照这个逻辑,任何不导致跨越64位字边界的对齐方式都是一样好的(它将在一次读取中读取变量)。但事实并非如此。 - undefined
@0___________ 你能再解释一下吗? - undefined

-1

uint32_t 占用 4 字节,2 * uint32_t = 8 字节,uint8_t 占用 1 字节,但由于最大变量大小为 4 字节,所以编译器将 uint8_t 扩展为 4 字节,现在我们有 12 字节 + uint32_t + uint8_t,得到 20 字节。假设我们有

struct azaza {
 uint32_t addr1;
 uint8_t tmp;
 uint8_t tmp1;
 uint8_t tmp2;
 uint32_t addr3;
 uint8_t flags;
};

在每个4字节的块中,大小变为4 + 3字节,在每个4字节的块中变为4 + 1字节 = 4 + 4 + 4 + 4 = 16

struct azaza { 
 uint32_t tmp;
 uint8_t tmp1;
 uint8_t tmp2;
 uint8_t tmp3;
 uint64_t tmp4;
 uint8_t tmp5;
};

最大的元素是8字节 tmptmptmptmptmp1tmp2tmp3-|tmp4tmp4tmp4tmp4tmp4tmp4tmp4tmp4|tmp5------- =24字节


-1

另一个例子

  struct azaza {

 uint8_t t1;
 uint16_t t2;
 uint32_t t3;
 
};

最大的元素是4字节。将“-”视为空块。t1-t2t2|t3t3t3t3=8字节


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