考虑:
struct mystruct_A
{
char a;
int b;
char c;
} x;
struct mystruct_B
{
int b;
char a;
} y;
结构的大小分别为 12 和 8。
这些结构是填充的还是包装好的?
何时进行填充或包装?
填充 将结构成员与“自然”地址边界对齐int
- 例如,成员将具有偏移量,这些偏移量mod(4) == 0
位于 32 位平台上。默认情况下填充是打开的。它将以下“间隙”插入到您的第一个结构中:
struct mystruct_A {
char a;
char gap_0[3]; /* inserted by compiler: for alignment of b */
int b;
char c;
char gap_1[3]; /* -"-: for alignment of the whole struct in an array */
} x;
另一方面,Packing__attribute__((__packed__))
会阻止编译器进行填充 - 这必须明确要求 - 在 GCC 下它是,所以如下:
struct __attribute__((__packed__)) mystruct_A {
char a;
int b;
char c;
};
将6
在 32 位架构上生成大小结构。
不过需要注意的是 - 在允许它的架构(如 x86 和 amd64)上,未对齐的内存访问速度较慢,并且在SPARC等严格对齐的架构上被明确禁止。
(上面的答案很清楚地解释了原因,但似乎对填充的大小并不完全清楚,所以,我将根据我从The Lost Art of Structure Packing中学到的知识添加一个答案,它已经发展到不限于C
,而是也适用于Go
, Rust
. )
规则:
int
应从可被 4 整除的地址开始,并long
从 8和short
2 整除的地址开始。char
并且char[]
很特殊,可以是任何内存地址,因此它们之前不需要填充。struct
,除了每个单独成员的对齐需要之外,整个结构本身的大小将对齐到可被最大单独成员的大小整除的大小,并在末尾填充。long
可以被 8 整除,int
然后被 4 整除,short
然后被 2 整除。会员顺序:
stu_c
和stu_d
from 下面的示例具有相同的成员,但顺序不同,并导致 2 个结构的大小不同。规则:
(n * 16)
字节开始。(您可以在下面的示例中看到,结构的所有打印十六进制地址都以 . 结尾0
。)long double
)。char
as 成员,它的地址可以从任何地址开始。空地:
test_struct_address()
下面,变量x
位于相邻的结构g
和h
. x
声明,其h
地址都不会改变,只是重新利用了浪费x
的空白空间。
的类似情况。g
y
(对于 64 位系统)
memory_align.c:
/**
* Memory align & padding - for struct.
* compile: gcc memory_align.c
* execute: ./a.out
*/
#include <stdio.h>
// size is 8, 4 + 1, then round to multiple of 4 (int's size),
struct stu_a {
int i;
char c;
};
// size is 16, 8 + 1, then round to multiple of 8 (long's size),
struct stu_b {
long l;
char c;
};
// size is 24, l need padding by 4 before it, then round to multiple of 8 (long's size),
struct stu_c {
int i;
long l;
char c;
};
// size is 16, 8 + 4 + 1, then round to multiple of 8 (long's size),
struct stu_d {
long l;
int i;
char c;
};
// size is 16, 8 + 4 + 1, then round to multiple of 8 (double's size),
struct stu_e {
double d;
int i;
char c;
};
// size is 24, d need align to 8, then round to multiple of 8 (double's size),
struct stu_f {
int i;
double d;
char c;
};
// size is 4,
struct stu_g {
int i;
};
// size is 8,
struct stu_h {
long l;
};
// test - padding within a single struct,
int test_struct_padding() {
printf("%s: %ld\n", "stu_a", sizeof(struct stu_a));
printf("%s: %ld\n", "stu_b", sizeof(struct stu_b));
printf("%s: %ld\n", "stu_c", sizeof(struct stu_c));
printf("%s: %ld\n", "stu_d", sizeof(struct stu_d));
printf("%s: %ld\n", "stu_e", sizeof(struct stu_e));
printf("%s: %ld\n", "stu_f", sizeof(struct stu_f));
printf("%s: %ld\n", "stu_g", sizeof(struct stu_g));
printf("%s: %ld\n", "stu_h", sizeof(struct stu_h));
return 0;
}
// test - address of struct,
int test_struct_address() {
printf("%s: %ld\n", "stu_g", sizeof(struct stu_g));
printf("%s: %ld\n", "stu_h", sizeof(struct stu_h));
printf("%s: %ld\n", "stu_f", sizeof(struct stu_f));
struct stu_g g;
struct stu_h h;
struct stu_f f1;
struct stu_f f2;
int x = 1;
long y = 1;
printf("address of %s: %p\n", "g", &g);
printf("address of %s: %p\n", "h", &h);
printf("address of %s: %p\n", "f1", &f1);
printf("address of %s: %p\n", "f2", &f2);
printf("address of %s: %p\n", "x", &x);
printf("address of %s: %p\n", "y", &y);
// g is only 4 bytes itself, but distance to next struct is 16 bytes(on 64 bit system) or 8 bytes(on 32 bit system),
printf("space between %s and %s: %ld\n", "g", "h", (long)(&h) - (long)(&g));
// h is only 8 bytes itself, but distance to next struct is 16 bytes(on 64 bit system) or 8 bytes(on 32 bit system),
printf("space between %s and %s: %ld\n", "h", "f1", (long)(&f1) - (long)(&h));
// f1 is only 24 bytes itself, but distance to next struct is 32 bytes(on 64 bit system) or 24 bytes(on 32 bit system),
printf("space between %s and %s: %ld\n", "f1", "f2", (long)(&f2) - (long)(&f1));
// x is not a struct, and it reuse those empty space between struts, which exists due to padding, e.g between g & h,
printf("space between %s and %s: %ld\n", "x", "f2", (long)(&x) - (long)(&f2));
printf("space between %s and %s: %ld\n", "g", "x", (long)(&x) - (long)(&g));
// y is not a struct, and it reuse those empty space between struts, which exists due to padding, e.g between h & f1,
printf("space between %s and %s: %ld\n", "x", "y", (long)(&y) - (long)(&x));
printf("space between %s and %s: %ld\n", "h", "y", (long)(&y) - (long)(&h));
return 0;
}
int main(int argc, char * argv[]) {
test_struct_padding();
// test_struct_address();
return 0;
}
执行结果 - test_struct_padding()
:
stu_a: 8
stu_b: 16
stu_c: 24
stu_d: 16
stu_e: 16
stu_f: 24
stu_g: 4
stu_h: 8
执行结果 - test_struct_address()
:
stu_g: 4
stu_h: 8
stu_f: 24
address of g: 0x7fffd63a95d0 // struct variable - address dividable by 16,
address of h: 0x7fffd63a95e0 // struct variable - address dividable by 16,
address of f1: 0x7fffd63a95f0 // struct variable - address dividable by 16,
address of f2: 0x7fffd63a9610 // struct variable - address dividable by 16,
address of x: 0x7fffd63a95dc // non-struct variable - resides within the empty space between struct variable g & h.
address of y: 0x7fffd63a95e8 // non-struct variable - resides within the empty space between struct variable h & f1.
space between g and h: 16
space between h and f1: 16
space between f1 and f2: 32
space between x and f2: -52
space between g and x: 12
space between x and y: 12
space between h and y: 8
因此每个变量的地址开始是 g:d0 x:dc h:e0 y:e8
我知道这个问题很老,这里的大多数答案都很好地解释了填充,但是在我自己试图理解它的同时,我认为对正在发生的事情有一个“视觉”的形象很有帮助。
处理器以确定大小(字)的“块”读取内存。假设处理器字长 8 个字节。它将内存视为一大排 8 字节的构建块。每次它需要从内存中获取一些信息时,它都会到达其中一个块并获取它。
如上图所示,Char(1 字节长)在哪里并不重要,因为它将位于其中一个块内,只需要 CPU 处理 1 个字。
当我们处理大于 1 字节的数据时,例如 4 字节 int 或 8 字节 double,它们在内存中的对齐方式会影响 CPU 必须处理的字数。如果 4 字节块以某种方式对齐,它们总是适合块的内部(内存地址是 4 的倍数),则只需处理一个字。否则,一块 4 字节的块可能有一部分在一个块上,另一部分在另一个块上,需要处理器处理 2 个字来读取此数据。
这同样适用于 8 字节双精度,但现在它必须位于 8 的内存地址倍数中,以确保它始终位于块内。
这考虑了一个 8 字节的字处理器,但该概念适用于其他大小的字。
填充通过填充这些数据之间的间隙来工作,以确保它们与这些块对齐,从而提高读取内存时的性能。
但是,正如其他答案所述,有时空间比性能本身更重要。也许您在没有太多 RAM 的计算机上处理大量数据(可以使用交换空间,但速度要慢得多)。您可以在程序中安排变量,直到完成最少的填充(因为它在其他一些答案中得到了很好的例证),但如果这还不够,您可以显式禁用填充,这就是包装。
结构填充抑制结构填充,在对齐最重要时使用填充,在空间最重要时使用填充。
一些编译器提供#pragma
抑制填充或使其打包到 n 个字节。有些提供关键字来做到这一点。通常用于修改结构填充的 pragma 将采用以下格式(取决于编译器):
#pragma pack(n)
例如,ARM 提供了__packed
抑制结构填充的关键字。仔细阅读您的编译器手册以了解有关此内容的更多信息。
因此,打包结构是没有填充的结构。
通常将使用打包结构
节省空间
使用某种协议格式化数据结构以通过网络传输(这当然不是一个好习惯,因为您需要
处理字节序)
填充和打包只是同一事物的两个方面:
在mystruct_A
中,假设默认对齐方式为 4,则每个成员都以 4 字节的倍数对齐。由于 的大小char
为 1,因此a
和的填充为c
4 - 1 = 3 个字节,而已经是 4 个字节的不需要填充int b
。它的工作方式相同mystruct_B
。
填充规则:
为什么规则 2:考虑以下结构,
如果我们要创建这个结构的数组(2 个结构),最后不需要填充:
因此,结构的大小 = 8 个字节
假设我们要创建另一个结构,如下所示:
如果我们要创建此结构的数组,则有两种可能性,即最后所需的填充字节数。
A. 如果我们在末尾添加 3 个字节并将其对齐为 int 而不是 Long:
B. 如果我们在末尾添加 7 个字节并对齐为 Long:
第二个数组的起始地址是8的倍数(即24)。结构的大小 = 24 字节
因此,通过将结构的下一个数组的起始地址与最大成员的倍数对齐(即,如果我们要创建此结构的数组,则第二个数组的第一个地址必须从一个倍数的地址开始结构的最大成员。这里是 24(3 * 8)),我们可以计算出最后需要的填充字节数。
变量存储在可被其对齐(通常是其大小)整除的任何地址。因此,填充/打包不仅适用于结构。实际上,所有数据都有自己的对齐要求:
int main(void) {
// We assume the `c` is stored as first byte of machine word
// as a convenience! If the `c` was stored as a last byte of previous
// word, there is no need to pad bytes before variable `i`
// because `i` is automatically aligned in a new word.
char c; // starts from any addresses divisible by 1(any addresses).
char pad[3]; // not-used memory for `i` to start from its address.
int32_t i; // starts from any addresses divisible by 4.
这与struct类似,但有一些区别。首先,我们可以说有两种填充—— a) 为了正确地从每个成员的地址开始,在成员之间插入一些字节。b)为了正确地从其地址开始下一个结构实例,一些字节被附加到每个结构:
// Example for rule 1 below.
struct st {
char c; // starts from any addresses divisible by 4, not 1.
char pad[3]; // not-used memory for `i` to start from its address.
int32_t i; // starts from any addresses divisible by 4.
};
// Example for rule 2 below.
struct st {
int32_t i; // starts from any addresses divisible by 4.
char c; // starts from any addresses.
char pad[3]; // not-used memory for next `st`(or anything that has same
// alignment requirement) to start from its own address.
};
4
对齐int32_t
)确定。这与普通变量不同。普通变量可以开始任何可被其对齐方式整除的地址,但结构的第一个成员并非如此。如您所知,结构的地址与其第一个成员的地址相同。struct st arr[2];
。为了使arr[1]
(arr[1]
的第一个成员) 从可被 4 整除的地址开始,我们应该在每个结构的末尾附加 3 个字节。这是我从The Lost Art of Structure Packing中学到的。
注意:您可以通过_Alignof
运算符调查数据类型的对齐要求。offsetof
此外,您可以通过宏在结构内获取成员的偏移量。
这些结构是填充的还是包装好的?
他们是软垫的。
最初想到的唯一可能是它们可以被打包的地方是大小是否相同char
,int
因此结构的最小尺寸char/int/char
将允许没有填充,int/char
结构也是如此。
但是,这将需要sizeof(int)
和sizeof(char)
都是四个(以获得十二个和八个尺寸)。整个理论分崩离析,因为它得到了始终为一的标准sizeof(char)
的保证。
如果char
宽度int
相同,尺寸将是一加一,而不是四加四。因此,为了获得 12 的大小,在最后一个字段之后必须有填充。
何时进行填充或包装?
每当编译器实现想要它时。编译器可以自由地在字段之间插入填充,并在最后一个字段之后(但不能在第一个字段之前)。
这通常是为了提高性能,因为某些类型在特定边界对齐时性能更好。如果您尝试访问未对齐的数据(是的,我在看着您, ARM),甚至有些架构会拒绝运行(即崩溃)。
您通常可以使用特定于实现的功能(例如#pragma pack
. 即使您在特定实现中无法做到这一点,您也可以在编译时检查代码以确保它满足您的要求(使用标准 C 功能,而不是特定于实现的东西)。
例如:
// C11 or better ...
#include <assert.h>
struct strA { char a; int b; char c; } x;
struct strB { int b; char a; } y;
static_assert(sizeof(struct strA) == sizeof(char)*2 + sizeof(int), "No padding allowed");
static_assert(sizeof(struct strB) == sizeof(char) + sizeof(int), "No padding allowed");
如果这些结构中有任何填充,这样的东西将拒绝编译。
没有任何问题!想要掌握主题必须做到以下几点,
- 细读Eric S. Raymond 所写的《结构包装的失落艺术》
- 浏览Eric 的代码示例
- 最后但同样重要的是,不要忘记以下关于填充 结构与最大类型的对齐要求对齐的规则。
仅当您明确告诉编译器打包结构时,才完成结构打包。填充是您所看到的。您的 32 位系统正在将每个字段填充到字对齐。如果您告诉编译器打包结构,它们将分别为 6 和 5 个字节。不要那样做。它不是可移植的,并且会使编译器生成更慢(有时甚至是错误)的代码。
数据结构对齐是数据在计算机内存中排列和访问的方式。它由两个独立但相关的问题组成:数据对齐和数据结构填充。当现代计算机读取或写入内存地址时,它将以字大小的块(例如 32 位系统上的 4 字节块)或更大的形式执行此操作。数据对齐意味着将数据放置在等于字大小的某个倍数的内存地址上,由于 CPU 处理内存的方式,这会提高系统的性能。为了对齐数据,可能需要在最后一个数据结构的结尾和下一个数据结构的开头之间插入一些无意义的字节,这就是数据结构填充。