结构填充
Structure Padding
我知道编译器会进行结构填充,因为 CPU 一次可以读取一个单词(单词大小取决于体系结构 16/32/64)但我无法理解以下示例的填充行为。
struct pad
{
char ch;
};
"struct pad" 的大小为 1 字节(但根据填充概念我怀疑是 4 字节(32 位架构)。
这里,struct实际上有1B大小,这里不需要填充。
实际结构填充如下:
#include <stdio.h>
struct char_int {
char a;
// 3B padding here.
int b;
};
int main() {
struct char_int struct_a;
struct_a.a = 0;
struct_a.b = 12;
printf("sizeof(struct_a):%d, &struct_a.a:0x%x, &struct_a.b:0x%x\n",
sizeof(struct char_int), &struct_a.a, &struct_a.b);
return 0;
}
在我的环境中,GCC 4.9.2 RHEL AMD64 5.9。输出如下:
sizeof(struct_a):8, &struct_a.a:0x36a41fd0, &struct_a.b:0x36a41fd4
在栈或堆中为上述结构分配内存时,char a
和int b
之间会填充3个字节,因为这里字段char_int.b
有地址偏移量4
,而文件 char_int.a
只有 1 个字节。
char 类型可以在任何地址边界上高效访问,因此不需要填充。
已定义结构的大小为 1 个字节,此处未使用填充。因为char
占用1字节内存,没有其他数据类型。
struct s1
{
char a;
char b;
}
此 returns 结构大小为 2
。当使用不同大小的变量时,结构填充是为了更容易和更快地处理地址。
您混淆了内存填充和内存控制器寻址(通常对字进行操作)的概念。
出于各种原因,C 强制执行标准结构对齐要求(尽管通常可以使用编译器 #pragma
s 覆盖这些要求)。按照标准,你定义的struct确实应该有1个字节的大小。
然而,当在 x86 CPU 上执行此代码时,内存控制器将加载 4 字节对齐的字,并且 CPU 将为结构提取 1 字节的值。无论结构是否为 4 字节对齐,都会发生这种情况,并且对性能没有影响。
首先,它是编译器,而不是添加填充的 CPU — 但是编译器会根据特定目标 CPU 功能(主要是其数据总线宽度)添加填充。
添加了填充字节以提高内存访问效率。当 CPU 通过 16 位总线在偶地址上读取或写入 2 字节字时,传输可以在一个周期内完成。但是,如果 2 字节数据位于奇地址,它会占用内存中一个偶地址字的 'lower' 一半和另一个偶地址字的 'upper' 一半。 CPU 然后必须执行两个总线周期:一个用于数据占用的每个内存字,每次只使用一半的总线宽度进行实际传输。
对于 32 位总线,如果 2 字节或 4 字节的数据跨越双字(4 字节)边界,则它也可能需要两个周期。类似的效果发生在 64 位总线上。这就是填充的原因:编译器添加字节以将数据对齐到可以有效访问它们的边界。
如果较短的成员后跟较长的成员,则可能会发生填充。
仅包含 1 字节成员的结构
struct BBB {
byte b1;
byte b2;
byte b3;
}
不需要任何填充 - 每个 byte
都可以在一个周期内读取或写入。
类似
struct III {
int i1;
int i2;
int i3;
}
只要结构与偶数边界对齐(根据其第一个成员要求),所有成员也都正确对齐。
struct BI {
byte b;
// one byte added here
int i;
}
此处添加了一个字节,以便 2 字节 int
与 2 字节边界对齐。
struct IB {
int i;
byte b;
// one byte added here
}
惊喜!为什么要在末尾添加额外的字节?此处需要对齐哪些数据...?
好吧,假设你声明了一个数组:
struct IB arr[4];
那么数组数据在内存中是这样排列的:
int arr[0].i
byte arr[0].b
// byte arr[0] padding
int arr[1].i
byte arr[1].b
// byte arr[1] padding
int arr[2].i
byte arr[2].b
// byte arr[2] padding
int arr[3].i
byte arr[3].b
// byte arr[3] padding
结构末尾的每个 byte
成员都得到 'extendend' 两个字节,因此下一个数组项中的 int
与偶数地址对齐。如果不填充,每个第二个数组项都会使其 int
成员未对齐。
对于 32 位总线,最多可以使用 3 个字节的填充:
struct BL {
byte b;
// 3 bytes added
long l;
}
因此 4 字节 long
位于 4 字节边界。
结构填充是一个棘手的概念。因此,如果您不容易理解它,则无需担心。
考虑任何结构 - 如果第一个元素的大小小于结构中的下一个元素,则只需要填充,在这种情况下,应对第一个元素进行结构填充。等待 !!!还没结束。
如果你想找到整个结构的大小,还有一个 logic/concept 要应用。即结构的最终大小应能被结构中最大尺寸的元素整除。
下面举例推导
struct test {
char a;
char b;
double d;
int m;
double dd;
char c;
float f;
double ddd;
char e;
};
现在让我们找出每个元素的大小。
char a;
1
char b;
1
double d;
8
int m;
4
double dd;
8
char c;
1
float f;
4
double ddd;
8
char e;
8
现在我们将应用结构填充。
char a;
1 (由于上述原因无需填充)
char b;
1 + 7 (因为 char 比 double 小,填充 'double size' 的 完成)*
double d;
8 (由于上述原因无需填充)
int m;
4 + 4 (因为 int 比 double 小,'double size' 的填充完成)
double dd;
8 (由于上述原因无需填充)
char c;
1 + 3*(因为 char 比 float 小,填充 'float size' 完成)*
float f;
4 + 4 (因为 float 比 double 小,填充 'double size' 完成)
double ddd;
8 (由于上述原因无需填充)
char e;
1 (由于上述原因无需填充)
现在,如果您将所有内容相加,当您需要应用可被结构的最大元素整除的第二个概念时,这里会出现 54。
这里是 8,所以 54 + 2 = 56 可以被 8 整除。
上述结构的最终大小为 56。
我知道编译器会进行结构填充,因为 CPU 一次可以读取一个单词(单词大小取决于体系结构 16/32/64)但我无法理解以下示例的填充行为。
struct pad
{
char ch;
};
"struct pad" 的大小为 1 字节(但根据填充概念我怀疑是 4 字节(32 位架构)。
这里,struct实际上有1B大小,这里不需要填充。
实际结构填充如下:
#include <stdio.h>
struct char_int {
char a;
// 3B padding here.
int b;
};
int main() {
struct char_int struct_a;
struct_a.a = 0;
struct_a.b = 12;
printf("sizeof(struct_a):%d, &struct_a.a:0x%x, &struct_a.b:0x%x\n",
sizeof(struct char_int), &struct_a.a, &struct_a.b);
return 0;
}
在我的环境中,GCC 4.9.2 RHEL AMD64 5.9。输出如下:
sizeof(struct_a):8, &struct_a.a:0x36a41fd0, &struct_a.b:0x36a41fd4
在栈或堆中为上述结构分配内存时,char a
和int b
之间会填充3个字节,因为这里字段char_int.b
有地址偏移量4
,而文件 char_int.a
只有 1 个字节。
char 类型可以在任何地址边界上高效访问,因此不需要填充。
已定义结构的大小为 1 个字节,此处未使用填充。因为char
占用1字节内存,没有其他数据类型。
struct s1
{
char a;
char b;
}
此 returns 结构大小为 2
。当使用不同大小的变量时,结构填充是为了更容易和更快地处理地址。
您混淆了内存填充和内存控制器寻址(通常对字进行操作)的概念。
出于各种原因,C 强制执行标准结构对齐要求(尽管通常可以使用编译器 #pragma
s 覆盖这些要求)。按照标准,你定义的struct确实应该有1个字节的大小。
然而,当在 x86 CPU 上执行此代码时,内存控制器将加载 4 字节对齐的字,并且 CPU 将为结构提取 1 字节的值。无论结构是否为 4 字节对齐,都会发生这种情况,并且对性能没有影响。
首先,它是编译器,而不是添加填充的 CPU — 但是编译器会根据特定目标 CPU 功能(主要是其数据总线宽度)添加填充。
添加了填充字节以提高内存访问效率。当 CPU 通过 16 位总线在偶地址上读取或写入 2 字节字时,传输可以在一个周期内完成。但是,如果 2 字节数据位于奇地址,它会占用内存中一个偶地址字的 'lower' 一半和另一个偶地址字的 'upper' 一半。 CPU 然后必须执行两个总线周期:一个用于数据占用的每个内存字,每次只使用一半的总线宽度进行实际传输。
对于 32 位总线,如果 2 字节或 4 字节的数据跨越双字(4 字节)边界,则它也可能需要两个周期。类似的效果发生在 64 位总线上。这就是填充的原因:编译器添加字节以将数据对齐到可以有效访问它们的边界。
如果较短的成员后跟较长的成员,则可能会发生填充。
仅包含 1 字节成员的结构
struct BBB {
byte b1;
byte b2;
byte b3;
}
不需要任何填充 - 每个 byte
都可以在一个周期内读取或写入。
类似
struct III {
int i1;
int i2;
int i3;
}
只要结构与偶数边界对齐(根据其第一个成员要求),所有成员也都正确对齐。
struct BI {
byte b;
// one byte added here
int i;
}
此处添加了一个字节,以便 2 字节 int
与 2 字节边界对齐。
struct IB {
int i;
byte b;
// one byte added here
}
惊喜!为什么要在末尾添加额外的字节?此处需要对齐哪些数据...?
好吧,假设你声明了一个数组:
struct IB arr[4];
那么数组数据在内存中是这样排列的:
int arr[0].i
byte arr[0].b
// byte arr[0] padding
int arr[1].i
byte arr[1].b
// byte arr[1] padding
int arr[2].i
byte arr[2].b
// byte arr[2] padding
int arr[3].i
byte arr[3].b
// byte arr[3] padding
结构末尾的每个 byte
成员都得到 'extendend' 两个字节,因此下一个数组项中的 int
与偶数地址对齐。如果不填充,每个第二个数组项都会使其 int
成员未对齐。
对于 32 位总线,最多可以使用 3 个字节的填充:
struct BL {
byte b;
// 3 bytes added
long l;
}
因此 4 字节 long
位于 4 字节边界。
结构填充是一个棘手的概念。因此,如果您不容易理解它,则无需担心。 考虑任何结构 - 如果第一个元素的大小小于结构中的下一个元素,则只需要填充,在这种情况下,应对第一个元素进行结构填充。等待 !!!还没结束。 如果你想找到整个结构的大小,还有一个 logic/concept 要应用。即结构的最终大小应能被结构中最大尺寸的元素整除。
下面举例推导
struct test {
char a;
char b;
double d;
int m;
double dd;
char c;
float f;
double ddd;
char e;
};
现在让我们找出每个元素的大小。
char a;
1
char b;
1
double d;
8
int m;
4
double dd;
8
char c;
1
float f;
4
double ddd;
8
char e;
8
现在我们将应用结构填充。
char a;
1 (由于上述原因无需填充)
char b;
1 + 7 (因为 char 比 double 小,填充 'double size' 的 完成)*
double d;
8 (由于上述原因无需填充)
int m;
4 + 4 (因为 int 比 double 小,'double size' 的填充完成)
double dd;
8 (由于上述原因无需填充)
char c;
1 + 3*(因为 char 比 float 小,填充 'float size' 完成)*
float f;
4 + 4 (因为 float 比 double 小,填充 'double size' 完成)
double ddd;
8 (由于上述原因无需填充)
char e;
1 (由于上述原因无需填充)
现在,如果您将所有内容相加,当您需要应用可被结构的最大元素整除的第二个概念时,这里会出现 54。 这里是 8,所以 54 + 2 = 56 可以被 8 整除。
上述结构的最终大小为 56。