64位机器上的结构填充
structure padding on 64bit machine
struct A
{
uint32_t var1;
uint32_t var2;
uint32_t var3;
uint32_t var4;
uint32_t var5;
};
在上面的结构中,编译器没有填充并分配了 20 个字节。
现在我们有另一种结构,它包含一个 8 字节变量而不是两个 4 bytes.In 这种情况编译器填充并为该结构分配 24 字节。
struct B
{
uint32_t var1;
uint32_t var2;
uint32_t var3;
uint64_t var5;
};
为什么会有这样的行为?
如果编译器将数据对齐到 8 字节边界,那么第一个结构中应该有 4 字节的填充并且应该
在这种情况下不要填充第二个结构。而且如果编译器将数据对齐到 4 字节边界,那么为什么在第二个结构中有 4 字节的填充?
编译器:GCC
平台:64 位 linux , x86_64
首先,结构对齐不是一门精确的科学,可能取决于体系结构和编译器。
在很多情况下,所有的结构体成员都是根据最大的变量(以字节为单位)来填充的。在您的第一个结构中,所有变量都是 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 */
};
#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;
}
试试这个,它对我有用
虽然编译器可以根据他们认为合适的方式自由填充或不填充,但通常它们会将变量对齐在变量大小的倍数的边界上。
在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的倍数。所以在[=17之后需要填充4个字节=] 以允许 var5
正确对齐。
在 struct A
的情况下,所有字段的大小都是 4 个字节,因此不需要填充。如果您创建了这种类型的数组,例如 struct A a[5]
,a[1]
将在 a[0]
之后 20 个字节,a[2]
将在 a[1]
之后 20 个字节,并且很快。添加填充到 struct A
会浪费 space,因为所有子字段仍然在他们需要的 4 字节边界上对齐。
对齐规则(在 x86 和 x86_64 上)通常是根据变量的大小对齐变量。
换句话说,32位变量对齐4个字节,64位变量对齐8个字节,依此类推
在你的第二种情况下,
之间添加了 4 个字节的填充
uint32_t var3;
uint64_t var5;
让 var5
对齐 8 个字节。
出于这个原因,最好将数据成员从大到小排序(但由于数据局部性、可读性等因素,这并不那么简单)。
您的 struct B
中的填充几乎肯定不在末尾,而是在第三个 32 位成员之后:
struct B
{
uint32_t var1;
uint32_t var2;
uint32_t var3;
// 4-byte padding here
uint64_t var5;
};
这是因为 var1
到 var3
加起来为 12 个字节,不能被 8 整除。您的编译器希望 8 字节整数类型位于可被 8 整除的地址上。
在这种情况下,您还会在结构的末尾进行填充:
struct C
{
uint64_t memb1;
uint32_t memb2;
// Padding here
};
填充是为了 memb1
在 struct C
:
数组中对齐
struct C c_array[13];
当然 c_array[0].memb1
是对齐的,因为它位于数组的基地址。但是 c_array[1].memb1
呢?如果结构中没有填充,它就不会对齐。
C 的定义是数组元素之间不能添加填充;数组的元素是紧密分配的。因此,如果需要填充,则必须将其硬塞到元素类型中。结构的布局必须考虑可能的数组聚合。
虽然我确信存在例外情况,但通常编译器会插入足够的填充以满足对齐要求,无论是对于结构中的字段还是对于整个结构。 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))
。但是,必须格外小心地使用这些功能,因为它们很容易导致对齐违规。
struct A
{
uint32_t var1;
uint32_t var2;
uint32_t var3;
uint32_t var4;
uint32_t var5;
};
在上面的结构中,编译器没有填充并分配了 20 个字节。
现在我们有另一种结构,它包含一个 8 字节变量而不是两个 4 bytes.In 这种情况编译器填充并为该结构分配 24 字节。
struct B
{
uint32_t var1;
uint32_t var2;
uint32_t var3;
uint64_t var5;
};
为什么会有这样的行为? 如果编译器将数据对齐到 8 字节边界,那么第一个结构中应该有 4 字节的填充并且应该 在这种情况下不要填充第二个结构。而且如果编译器将数据对齐到 4 字节边界,那么为什么在第二个结构中有 4 字节的填充?
编译器:GCC 平台:64 位 linux , x86_64
首先,结构对齐不是一门精确的科学,可能取决于体系结构和编译器。
在很多情况下,所有的结构体成员都是根据最大的变量(以字节为单位)来填充的。在您的第一个结构中,所有变量都是 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 */
};
#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;
}
试试这个,它对我有用
虽然编译器可以根据他们认为合适的方式自由填充或不填充,但通常它们会将变量对齐在变量大小的倍数的边界上。
在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的倍数。所以在[=17之后需要填充4个字节=] 以允许 var5
正确对齐。
在 struct A
的情况下,所有字段的大小都是 4 个字节,因此不需要填充。如果您创建了这种类型的数组,例如 struct A a[5]
,a[1]
将在 a[0]
之后 20 个字节,a[2]
将在 a[1]
之后 20 个字节,并且很快。添加填充到 struct A
会浪费 space,因为所有子字段仍然在他们需要的 4 字节边界上对齐。
对齐规则(在 x86 和 x86_64 上)通常是根据变量的大小对齐变量。
换句话说,32位变量对齐4个字节,64位变量对齐8个字节,依此类推
在你的第二种情况下,
之间添加了 4 个字节的填充uint32_t var3;
uint64_t var5;
让 var5
对齐 8 个字节。
出于这个原因,最好将数据成员从大到小排序(但由于数据局部性、可读性等因素,这并不那么简单)。
您的 struct B
中的填充几乎肯定不在末尾,而是在第三个 32 位成员之后:
struct B
{
uint32_t var1;
uint32_t var2;
uint32_t var3;
// 4-byte padding here
uint64_t var5;
};
这是因为 var1
到 var3
加起来为 12 个字节,不能被 8 整除。您的编译器希望 8 字节整数类型位于可被 8 整除的地址上。
在这种情况下,您还会在结构的末尾进行填充:
struct C
{
uint64_t memb1;
uint32_t memb2;
// Padding here
};
填充是为了 memb1
在 struct C
:
struct C c_array[13];
当然 c_array[0].memb1
是对齐的,因为它位于数组的基地址。但是 c_array[1].memb1
呢?如果结构中没有填充,它就不会对齐。
C 的定义是数组元素之间不能添加填充;数组的元素是紧密分配的。因此,如果需要填充,则必须将其硬塞到元素类型中。结构的布局必须考虑可能的数组聚合。
虽然我确信存在例外情况,但通常编译器会插入足够的填充以满足对齐要求,无论是对于结构中的字段还是对于整个结构。 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))
。但是,必须格外小心地使用这些功能,因为它们很容易导致对齐违规。