结构填充如何针对 C 中最大的成员起作用?
How structure padding is works with respect to largest size member in C?
我在理解结构填充概念时有些困惑。我知道结构填充会以内存为代价提高处理器的性能。
这里我定义了一些结构
案例 1:
typedef struct{
double A; //8-byte
char B; //1-byte
char C: //1-byte
} Test1;
此处结构的总大小为 16 byte
(largest size member
为 double
。因此编译器为 aligned memory in the form of 8 byte
。)
但在这两种情况下
案例 2:
typedef struct{
int A; //4-byte
double B; //8-byte
float C; //4-byte
} Test2;
这里最大的成员是double (8 byte)
。所以这将分配 8 + 8 + 8 = 24 .
案例 3:
typedef struct{
double A; //8-byte
Int B; //4-byte
float C; //4-byte
} Test3;
不过这里最大的成员也是double (8 byte)
。所以理想情况下,这也与 24 字节相同。但是当我打印出值时,我得到的是大小 16 byte
。为什么它的行为不像 case 2
?有什么解释吗?
structure padding increases the performance of the processor
不一定只是性能 - 一些处理器甚至不能对未对齐的地址进行字访问。
关于你的例子,最大的成员是无关紧要的。重要的是 目标系统的首选对齐方式 。这是高度 CPU 特定的。一些可以在单个指令中访问更大数据(例如 64 位)的 CPUs 也具有对较小类型(例如 32 位)的指令支持并且使用比首选对齐更小的指令并不一定是很正常的降低性能。
就 C 语言而言,编译器可以在结构内的任何地方 插入填充 ,除了假定对齐的第一个成员之前。
这意味着如果没有特定的编译器和 CPU,讨论您的示例的行为是不可能或没有意义的。
一般来说,padding 是试图一次性 read/write 特定的成员。
例如,假设CPU一次只能读取8个字节,并且只有8的倍数地址才有效,也就是说可以访问0-7或8-15等
typedef struct{
int A; //4-byte
double B; //8-byte
float C; //4-byte
} Test2;
如果没有填充,访问B需要两次,因为B是4-11,先读0-7,再读7-15。所以它在 B 之前添加填充,以便 B 从 7 开始。
typedef struct{
double A; //8-byte
Int B; //4-byte
float C; //4-byte
} Test3;
这是不同的,因为可以一次访问所有成员。所以不需要填充。
注意对齐规则为:
- 对于结构体的每个成员,第一个成员的偏移量为0,后面成员的当前偏移量必须是当前成员类型的整数倍,不是最大的成员类型。
- 结构体中的所有数据成员在各自的内存中对齐后,结构体本身需要进行一次对齐,以保证整个结构体占用的内存是结构体中最大数据成员的最小整数倍结构
How structure padding is works with respect to largest size member in C?
填充从根本上取决于成员的对齐要求,而不仅仅是它们的大小。每个完整的对象类型都有一个对齐要求,它是某个数字 A,这样对象的地址必须始终是 A 的倍数。对齐要求始终是 2 的幂。
对象的大小总是其对齐要求的倍数,但对齐要求并不总是等于大小。例如,八字节 double
在某些 C 实现中可能具有四字节对齐。对齐要求通常是出于硬件方面的考虑,系统可能会在从内存加载或将其存储到内存时以四字节块处理八字节对象,因此即使是八字节,硬件也不会关心八字节对齐字节对象。为该系统设计的 C 实现可以使八字节 double
的对齐要求仅为四个字节。
对于您的示例,我们将使用一个字节对齐 char
、四个字节对齐一个四字节 float
、八个字节对齐一个八字节 double
.
情况一:
typedef struct{
double A; //8-byte
char B; //1-byte
char C: //1-byte
} Test1;
结构总是从要求的对齐边界开始,因为编译器会给结构本身一个对齐要求,等于它的任何成员的最严格对齐要求。 (C 标准也允许大于,但这在实践中并不常见。)然后 double A
占用八个字节。此时,char B
处于允许的位置,因为它的对齐要求只有一个字节,所以任何地址都是允许的。 char C
也可以。到目前为止,该结构的长度为 10 个字节。最后,该结构需要八字节对齐,才能始终满足 double
的对齐要求,因此该结构的总大小必须是八字节的倍数。为此,我们在末尾插入六个字节的填充,整个结构大小为 16 个字节。
案例2:
typedef struct{
int A; //4-byte
double B; //8-byte
float C; //4-byte
} Test2;
int A
从偏移量四开始。那么double B
需要从八字节的倍数开始,所以插入了四个字节的padding。现在我们最多有 16 个字节:四个用于 int A
,四个用于填充,八个用于 double B
。然后 float C
处于一个好的位置。它增加了四个字节,我们最多是 20 个字节。结构大小需要是八个字节的倍数,所以我们添加四个字节的填充,总共 24 个字节。
案例3:
typedef struct{
double A; //8-byte
int B; //4-byte [Typo fixed; was "Int".]
float C; //4-byte
} Test3;
double A
就是八个字节,然后int B
加四个字节。现在我们是 12 字节。这对 float C
没问题,因为它的对齐要求是四个字节,而 12 是四的倍数。 float
向结构中添加了四个字节,因此大小现在为 16 个字节。这符合结构的对齐要求,八个字节,因为 16 是八的倍数。所以我们不需要添加任何填充,整个结构大小为16字节。
编译器通常使用以下方法来确定结构中的填充:
- 结构中的每个成员都有一些大小 s 和一些对齐要求 a.
- 编译器开始时大小 S 设置为零,对齐要求 A 设置为一(字节)。
- 编译器按顺序处理结构中的每个成员:
- 考虑成员's对齐要求a。如果 S 当前不是 a 的倍数,则添加足够的字节 S 使其成为a 的倍数。这决定了成员将去哪里;它将从结构的开头偏移 S(对于 S 的当前值)。
- 设A为A和[=64=的最小公倍数1 ]a.
- 将s添加到S,为成员预留space。
- 当对每个成员都完成上述过程后,再考虑结构's的对齐要求A。如果 S 当前不是 A 的倍数,则只需添加 S 即可A.
的倍数
结构的大小就是上面做完后S的值
另外:
- 如果任何成员是一个数组,它的大小就是元素的个数乘以每个元素的大小,它的对齐要求就是一个元素的对齐要求。
- 如果任何成员是结构,其大小和对齐要求按上述计算。
- 如果任何成员是联合,其大小是其最大成员的大小加上刚好足以使其成为所有对齐的最小公倍数1 的倍数成员们。
对于基本类型(int
、double
等),对齐要求是实现定义的,通常主要由硬件决定。在许多处理器上,当数据具有特定对齐方式时(通常当其在内存中的地址是其大小的倍数时)加载和存储数据会更快。除此之外,上述规则主要来自逻辑;他们将每个成员放在必须满足对齐要求的位置,而不使用不必要的 space。
脚注
1 对于一般情况,我将其表述为使用对齐要求的最小公倍数。但是,由于对齐要求始终是 2 的幂,因此任何一组对齐要求的最小公倍数是其中最大的。
我在理解结构填充概念时有些困惑。我知道结构填充会以内存为代价提高处理器的性能。 这里我定义了一些结构
案例 1:
typedef struct{
double A; //8-byte
char B; //1-byte
char C: //1-byte
} Test1;
此处结构的总大小为 16 byte
(largest size member
为 double
。因此编译器为 aligned memory in the form of 8 byte
。)
但在这两种情况下
案例 2:
typedef struct{
int A; //4-byte
double B; //8-byte
float C; //4-byte
} Test2;
这里最大的成员是double (8 byte)
。所以这将分配 8 + 8 + 8 = 24 .
案例 3:
typedef struct{
double A; //8-byte
Int B; //4-byte
float C; //4-byte
} Test3;
不过这里最大的成员也是double (8 byte)
。所以理想情况下,这也与 24 字节相同。但是当我打印出值时,我得到的是大小 16 byte
。为什么它的行为不像 case 2
?有什么解释吗?
structure padding increases the performance of the processor
不一定只是性能 - 一些处理器甚至不能对未对齐的地址进行字访问。
关于你的例子,最大的成员是无关紧要的。重要的是 目标系统的首选对齐方式 。这是高度 CPU 特定的。一些可以在单个指令中访问更大数据(例如 64 位)的 CPUs 也具有对较小类型(例如 32 位)的指令支持并且使用比首选对齐更小的指令并不一定是很正常的降低性能。
就 C 语言而言,编译器可以在结构内的任何地方 插入填充 ,除了假定对齐的第一个成员之前。
这意味着如果没有特定的编译器和 CPU,讨论您的示例的行为是不可能或没有意义的。
一般来说,padding 是试图一次性 read/write 特定的成员。
例如,假设CPU一次只能读取8个字节,并且只有8的倍数地址才有效,也就是说可以访问0-7或8-15等
typedef struct{
int A; //4-byte
double B; //8-byte
float C; //4-byte
} Test2;
如果没有填充,访问B需要两次,因为B是4-11,先读0-7,再读7-15。所以它在 B 之前添加填充,以便 B 从 7 开始。
typedef struct{
double A; //8-byte
Int B; //4-byte
float C; //4-byte
} Test3;
这是不同的,因为可以一次访问所有成员。所以不需要填充。
注意对齐规则为:
- 对于结构体的每个成员,第一个成员的偏移量为0,后面成员的当前偏移量必须是当前成员类型的整数倍,不是最大的成员类型。
- 结构体中的所有数据成员在各自的内存中对齐后,结构体本身需要进行一次对齐,以保证整个结构体占用的内存是结构体中最大数据成员的最小整数倍结构
How structure padding is works with respect to largest size member in C?
填充从根本上取决于成员的对齐要求,而不仅仅是它们的大小。每个完整的对象类型都有一个对齐要求,它是某个数字 A,这样对象的地址必须始终是 A 的倍数。对齐要求始终是 2 的幂。
对象的大小总是其对齐要求的倍数,但对齐要求并不总是等于大小。例如,八字节 double
在某些 C 实现中可能具有四字节对齐。对齐要求通常是出于硬件方面的考虑,系统可能会在从内存加载或将其存储到内存时以四字节块处理八字节对象,因此即使是八字节,硬件也不会关心八字节对齐字节对象。为该系统设计的 C 实现可以使八字节 double
的对齐要求仅为四个字节。
对于您的示例,我们将使用一个字节对齐 char
、四个字节对齐一个四字节 float
、八个字节对齐一个八字节 double
.
情况一:
typedef struct{
double A; //8-byte
char B; //1-byte
char C: //1-byte
} Test1;
结构总是从要求的对齐边界开始,因为编译器会给结构本身一个对齐要求,等于它的任何成员的最严格对齐要求。 (C 标准也允许大于,但这在实践中并不常见。)然后 double A
占用八个字节。此时,char B
处于允许的位置,因为它的对齐要求只有一个字节,所以任何地址都是允许的。 char C
也可以。到目前为止,该结构的长度为 10 个字节。最后,该结构需要八字节对齐,才能始终满足 double
的对齐要求,因此该结构的总大小必须是八字节的倍数。为此,我们在末尾插入六个字节的填充,整个结构大小为 16 个字节。
案例2:
typedef struct{
int A; //4-byte
double B; //8-byte
float C; //4-byte
} Test2;
int A
从偏移量四开始。那么double B
需要从八字节的倍数开始,所以插入了四个字节的padding。现在我们最多有 16 个字节:四个用于 int A
,四个用于填充,八个用于 double B
。然后 float C
处于一个好的位置。它增加了四个字节,我们最多是 20 个字节。结构大小需要是八个字节的倍数,所以我们添加四个字节的填充,总共 24 个字节。
案例3:
typedef struct{
double A; //8-byte
int B; //4-byte [Typo fixed; was "Int".]
float C; //4-byte
} Test3;
double A
就是八个字节,然后int B
加四个字节。现在我们是 12 字节。这对 float C
没问题,因为它的对齐要求是四个字节,而 12 是四的倍数。 float
向结构中添加了四个字节,因此大小现在为 16 个字节。这符合结构的对齐要求,八个字节,因为 16 是八的倍数。所以我们不需要添加任何填充,整个结构大小为16字节。
编译器通常使用以下方法来确定结构中的填充:
- 结构中的每个成员都有一些大小 s 和一些对齐要求 a.
- 编译器开始时大小 S 设置为零,对齐要求 A 设置为一(字节)。
- 编译器按顺序处理结构中的每个成员:
- 考虑成员's对齐要求a。如果 S 当前不是 a 的倍数,则添加足够的字节 S 使其成为a 的倍数。这决定了成员将去哪里;它将从结构的开头偏移 S(对于 S 的当前值)。
- 设A为A和[=64=的最小公倍数1 ]a.
- 将s添加到S,为成员预留space。
- 当对每个成员都完成上述过程后,再考虑结构's的对齐要求A。如果 S 当前不是 A 的倍数,则只需添加 S 即可A. 的倍数
结构的大小就是上面做完后S的值
另外:
- 如果任何成员是一个数组,它的大小就是元素的个数乘以每个元素的大小,它的对齐要求就是一个元素的对齐要求。
- 如果任何成员是结构,其大小和对齐要求按上述计算。
- 如果任何成员是联合,其大小是其最大成员的大小加上刚好足以使其成为所有对齐的最小公倍数1 的倍数成员们。
对于基本类型(int
、double
等),对齐要求是实现定义的,通常主要由硬件决定。在许多处理器上,当数据具有特定对齐方式时(通常当其在内存中的地址是其大小的倍数时)加载和存储数据会更快。除此之外,上述规则主要来自逻辑;他们将每个成员放在必须满足对齐要求的位置,而不使用不必要的 space。
脚注
1 对于一般情况,我将其表述为使用对齐要求的最小公倍数。但是,由于对齐要求始终是 2 的幂,因此任何一组对齐要求的最小公倍数是其中最大的。