64位机器上的结构填充

Posted

技术标签:

【中文标题】64位机器上的结构填充【英文标题】:structure padding on 64bit machine 【发布时间】:2016-12-11 19:18:27 【问题描述】:
struct A

    uint32_t var1;
    uint32_t var2;
    uint32_t var3;
    uint32_t var4;
    uint32_t var5;
;

在上述结构中,编译器不填充并分配 20 个字节。

现在我们有了另一个结构,它包含一个 8 字节变量而不是两个 4 字节。在这种情况下,编译器填充并分配 24 个字节给这个结构。

struct B

    uint32_t var1;
    uint32_t var2;
    uint32_t var3;
    uint64_t var5;
;

为什么会有这样的行为? 如果编译器将数据对齐到 8 字节边界,那么第一个结构中应该有 4 个字节的填充,并且应该 在这种情况下不要填充第二个结构。并且如果编译器将数据对齐到 4 字节边界,那么为什么在第二个结构中有 4 字节的填充?

编译器:GCC 平台:64位linux,x86_64

【问题讨论】:

结构的对齐要求是对齐要求最严格的成员的对齐。在您的 C 实现中,uint64_t 的要求比 uint32_t 更严格。 Why does an uint64_t needs more memory than 2 uint32_t's when used in a class? And how to prevent this?的可能重复 在没有任何填充的情况下,uint64_t 将从偏移量 12 开始。这会使其未对齐,而不是 8 的倍数,因此编译器会插入 4 个字节的填充以使其达到偏移量 16。16 + 8 = 24 . 不需要额外的填充,当它存储在 B[] 中时仍然是对齐的,所以 24 就足够了。 为自己的方便而填充结构的编译器在处理具有刚性未填充内部结构的硬件时将毫无用处。伙计们,这是 C 语言——旨在编写操作系统。 【参考方案1】:

对齐规则(在 x86 和 x86_64 上)通常是根据变量的大小对齐变量。

换句话说,32 位变量在 4 个字节上对齐,64 位变量在 8 个字节上对齐,等等。

在第二种情况下,在它们之间添加了 4 个字节的填充

uint32_t var3;
uint64_t var5;

var5 对齐 8 个字节。

因此,最好将数据成员从大到小排序(但由于数据局部性、可读性等原因,这并不是那么简单)。

【讨论】:

【参考方案2】:

首先,结构对齐不是一门精确的科学,可能取决于架构和编译器。

在许多情况下,所有结构成员都根据最大变量(以字节为单位)进行填充。在您的第一个结构上,所有变量都是uint32_t,长度为 4 字节。然后,您的结构大小等于sizeof(uint32_t) * 5 = 4 * 5 = 20

在您的第二个结构中,最大的元素是uint64_t,其大小为 8 个字节。所以所有元素都将按照 8 个字节进行填充。

前两个uint32_t 填充在一起,但第三个不能正确填充:如果用下一个整数填充,uint64_t 将一分为二!因此编译器决定让这个uint32_t 独立运行,以避免拆分uint64_t

以下是您的结构示例以及所有变量的地址可能是什么:

struct A

  uint32_t var1;   /* ..00 */
  uint32_t var2;   /* ..04 */
  uint32_t var3;   /* ..08 */
  uint32_t var4;   /* ..12 */
  uint32_t var5;   /* ..16 */
;

struct B

  uint32_t var1;   /* ..00 */
  uint32_t var2;   /* ..04 */
  uint32_t var3;   /* ..08 */
  uint64_t var5;   /* ..16 */
;

【讨论】:

不错,但也许将“地址”更改为“偏移量”【参考方案3】:
#include <stdio.h>

typedef struct __atribute__((packed)) A 
    uint32_t var1;
    uint32_t var2;
    uint32_t var3;
    uint32_t var4;
    uint32_t var5;
 A ;


typedef struct __atribute__((packed)) B 
    uint32_t var1;
    uint32_t var2;
    uint32_t var3;
    uint64_t var4;
 B;


int main()

    printf("sizeof(A): %d sizeof(B): %d", sizeof(A), sizeof(B));

    return 0;

试试这个,它对我有用

【讨论】:

【参考方案4】:

struct B 中的填充几乎肯定不是在末尾,而是在第三个 32 位成员之后:

struct B

    uint32_t var1;
    uint32_t var2;
    uint32_t var3;
    // 4-byte padding here
    uint64_t var5;
;

这是因为 var1var3 加起来是 12 个字节,不能被 8 整除。您的编译器希望 8 字节整数类型位于可被 8 整除的地址上。

在这种情况下,您还会在结构的末尾获得填充:

struct C

   uint64_t memb1;
   uint32_t memb2;
   // Padding here
;

该填充是为了在struct C 的数组中对齐memb1

struct C c_array[13];

当然c_array[0].memb1 是对齐的,因为它位于数组的基地址。但是c_array[1].memb1 呢?如果结构中没有填充,则不会对齐。

C 的定义方式是不能在数组元素之间添加填充;数组的元素是紧密分配的。因此,如果需要填充,则必须将其硬塞到元素类型中。结构的布局必须考虑可能的数组聚合。

【讨论】:

【参考方案5】:

尽管编译器可以随意填充或不填充,但通常它们会在边界上对齐变量,该边界是变量大小的倍数。

struct 的情况下,填充基于最大原始元素的大小。如果是第二个struct B,那就是var5,它的类型是uint64_t

带有隐式填充的struct B的布局如下:

struct B

    uint32_t var1;      // offset 0
    uint32_t var2;      // offset 4
    uint32_t var3;      // offset 8
    uint32_t padding;   // offset 12
    uint64_t var5;      // offset 16
;

如果var5 紧跟在var3 之后,它将位于字节偏移12 处,它不是8 的倍数。所以var3 之后需要有4 个字节的填充以允许var5正确对齐。

struct A 的情况下,所有字段的大小都是4 字节,因此不需要填充。如果您创建了这种类型的数组,例如struct A a[5],则a[1] 将在a[0] 之后20 个字节,a[2] 将在a[1] 之后20 个字节,依此类推。向struct A 添加填充会浪费空间,因为所有子字段仍然在它们需要的 4 字节边界上对齐。

【讨论】:

【参考方案6】:

虽然我确信有例外,但通常编译器会插入足够的填充以满足对齐要求,无论是对于结构内的字段还是对于整个结构。 C 编译器不允许对结构中的字段重新排序。

不同的类型可以有不同的对齐要求,一个类型的大小必须是它的对齐要求的倍数。在大多数 64 位系统上,标准 C 基元类型的对齐要求与其大小相等。结构的对齐要求通常等于其成员的最高对齐要求。

有时必须对结构进行填充,以确保其成员满足对齐要求,并且整个结构的大小是其对齐要求的倍数。

让我们看看你的结构,再加上第三个结构。

struct A

    uint32_t var1; //size 4, alignment 4, offset 0
    uint32_t var2; //size 4, alignment 4, offset 4
    uint32_t var3; //size 4, alignment 4, offset 8
    uint32_t var4; //size 4, alignment 4, offset 12
    uint32_t var5; //size 4, alignment 4, offset 16
;

所有字段都是 uint32_t 类型,大小为 4,对齐方式为 4。因此不需要填充,结构的总大小为 20 字节,总对齐方式为 4 字节。

struct B

    uint32_t var1; //size 4, alignment 4, offset 0
    uint32_t var2; //size 4, alignment 4, offset 4
    uint32_t var3; //size 4, alignment 4, offset 8
    //4 bytes of padding.
    uint64_t var5; //size 8, alignment 8, offset 16
;

前三个字段的大小和对齐方式为 4,因此可以在没有填充的情况下分配它们。但是 var5 的大小和对齐方式为 8,因此不能在偏移量 12 处分配。必须插入填充并在偏移量 16 处分配 var5。整个结构的大小为 24,对齐方式为 8。

struct C

    uint32_t var1; //size 4, alignment 4, offset 0
    uint32_t var2; //size 4, alignment 4, offset 4
    uint64_t var3; //size 8, alignment 8, offset 8
    uint32_t var5; //size 4, alignment 4, offset 16
    //4 bytes of padding
;

在这种情况下,所有变量都可以分配到合适的偏移量,而无需插入填充。然而,结构的总大小必须是其对齐要求的倍数(否则数组会破坏对齐),因此必须填充结构的末尾。同样,整个结构的大小为 24,对齐方式为 8。

一些编译器具有覆盖结构正常打包的机制,例如另一个答案中提到的__attribute__((packed))。但是,使用这些功能时必须格外小心,因为它们很容易导致对齐违规。

【讨论】:

以上是关于64位机器上的结构填充的主要内容,如果未能解决你的问题,请参考以下文章

32位机器上“双”结构成员的填充逻辑[重复]

64位系统中结构的内存分配

如何确定体系结构上字的大小(32 或 64)?

c_cpp 填充示例(在64位机器中),来自http://www.catb.org/esr/structure-packing/

64 位 Linux 机器上的 Android SDK

指定 64 位对齐