考虑:
struct mystruct_A
{
char a;
int b;
char c;
} x;
struct mystruct_B
{
int b;
char a;
} y;
结构的大小分别为 12 和 8。
这些结构是填充的还是包装好的?
何时进行填充或包装?
padding
让事情变得更大。 packing
使事情变得更小。完全不同。
填充 aligns 结构成员到“自然”地址边界 - 例如,int
成员将具有偏移量,在 32 位平台上是 mod(4) == 0
。默认情况下填充是打开的。它将以下“间隙”插入到您的第一个结构中:
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;
另一方面,打包会阻止编译器进行填充 - 这必须明确请求 - 在 GCC 下它是 __attribute__((__packed__))
,因此如下:
struct __attribute__((__packed__)) mystruct_A {
char a;
int b;
char c;
};
将在 32 位架构上生成大小为 6
的结构。
不过需要注意的是 - 在允许它的架构(如 x86 和 amd64)上,未对齐的内存访问速度较慢,并且在 SPARC 等严格对齐的架构上被明确禁止。
(上面的答案很清楚地解释了原因,但似乎并不完全清楚填充的大小,所以,我将根据我从 The Lost Art of Structure Packing 中学到的知识添加一个答案,它有演变为不限于C
,还适用于Go
、Rust
。)
内存对齐(用于结构)
规则:
在每个单独的成员之前,都会有填充,以便使其从可以被其大小整除的地址开始。例如,在 64 位系统上,int 应从可被 4 整除的地址开始,长可被 8 整除,短可被 2 整除。
char 和 char[] 是特殊的,可以是任何内存地址,所以它们之前不需要填充。
对于结构,除了每个单独成员的对齐需要之外,整个结构本身的大小将对齐到可被最大单独成员的大小整除的大小,通过在末尾填充。例如,如果 struct 的最大成员是 long 则可被 8 整除,int 则可被 4 整除,short 则可被 2 整除。
会员顺序:
成员的顺序可能会影响结构的实际大小,因此请记住这一点。例如,下面示例中的 stu_c 和 stu_d 具有相同的成员,但顺序不同,并导致 2 个结构的大小不同。
内存中的地址(用于结构)
规则:
64 位系统结构体地址从 (n * 16) 个字节开始。 (您可以在下面的示例中看到,结构的所有打印十六进制地址都以 0 结尾。)原因:可能最大的单个结构成员是 16 个字节(长双精度)。
(更新)如果一个结构只包含一个 char 作为成员,它的地址可以从任何地址开始。
空的空间:
2 个结构之间的空白空间可以被可以放入的非结构变量使用。例如,在下面的 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
https://i.stack.imgur.com/jGaxj.png
<The Lost Art of C Structure Packing>
很好地解释了规则,甚至认为它比这个答案长一点。该书可在线免费获取:catb.org/esr/structure-packing
我知道这个问题很老,这里的大多数答案都很好地解释了填充,但是在我自己试图理解它的同时,我认为对正在发生的事情有一个“视觉”的形象很有帮助。
处理器以确定大小(字)的“块”读取内存。假设处理器字长 8 个字节。它将内存视为一大排 8 字节的构建块。每次它需要从内存中获取一些信息时,它都会到达其中一个块并获取它。
https://i.stack.imgur.com/VFz69.png
如上图所示,Char(1 字节长)在哪里并不重要,因为它将位于其中一个块内,只需要 CPU 处理 1 个字。
当我们处理大于 1 字节的数据时,例如 4 字节 int 或 8 字节 double,它们在内存中的对齐方式会影响 CPU 必须处理的字数。如果 4 字节块以某种方式对齐,它们总是适合块的内部(内存地址是 4 的倍数),则只需处理一个字。否则,一块 4 字节的块可能有一部分在一个块上,另一部分在另一个块上,需要处理器处理 2 个字来读取此数据。
这同样适用于 8 字节双精度,但现在它必须位于 8 的内存地址倍数中,以确保它始终位于块内。
这考虑了一个 8 字节的字处理器,但该概念适用于其他大小的字。
填充通过填充这些数据之间的间隙来工作,以确保它们与这些块对齐,从而提高读取内存时的性能。
但是,正如其他答案所述,有时空间比性能本身更重要。也许您在没有太多 RAM 的计算机上处理大量数据(可以使用交换空间,但速度要慢得多)。您可以在程序中安排变量,直到完成最少的填充(因为它在其他一些答案中得到了很好的例证),但如果这还不够,您可以显式禁用填充,这就是包装。
memcpy
等直接读入结构。
结构填充抑制结构填充,在对齐最重要时使用填充,在空间最重要时使用填充。
一些编译器提供 #pragma
来抑制填充或使其压缩到 n 个字节。有些提供关键字来做到这一点。通常用于修改结构填充的 pragma 将采用以下格式(取决于编译器):
#pragma pack(n)
例如,ARM 提供了 __packed
关键字来抑制结构填充。仔细阅读您的编译器手册以了解有关此内容的更多信息。
因此,打包结构是没有填充的结构。
通常将使用打包结构
节省空间
使用某种协议格式化数据结构以通过网络传输(这当然不是一个好习惯,因为您需要处理字节序)
填充和打包只是同一事物的两个方面:
包装或对齐是每个成员四舍五入的大小
padding 是为匹配对齐添加的额外空间
在 mystruct_A
中,假设默认对齐方式为 4,每个成员按 4 字节的倍数对齐。由于 char
的大小为 1,因此 a
和 c
的填充是 4 - 1 = 3 个字节,而 int b
已经是 4 个字节不需要填充。 mystruct_B
的工作方式相同。
填充规则:
结构的每个成员都应位于可被其大小整除的地址。在元素之间或结构末尾插入填充以确保满足此规则。这样做是为了让硬件更容易和更有效地访问总线。结构末尾的填充是根据结构中最大成员的大小决定的。
为什么规则 2:考虑以下结构,
https://i.stack.imgur.com/N6m9A.png
如果我们要创建这个结构的数组(2 个结构),最后不需要填充:
https://i.stack.imgur.com/0fNtE.png
因此,结构的大小 = 8 个字节
假设我们要创建另一个结构,如下所示:
https://i.stack.imgur.com/fVwTl.png
如果我们要创建此结构的数组,则有两种可能性,即最后所需的填充字节数。
A. 如果我们在末尾添加 3 个字节并将其对齐为 int 而不是 Long:
https://i.stack.imgur.com/PMRdV.png
B. 如果我们在末尾添加 7 个字节并对齐为 Long:
https://i.stack.imgur.com/2An9R.png
第二个数组的起始地址是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
宏在结构内获取成员的偏移量。
没有任何问题!想要掌握主题必须做到以下几点,
细读 Eric S. Raymond 编写的 The Lost Art of Structure Packing 阅读 Eric 的代码示例 最后但同样重要的是,不要忘记以下关于填充的规则,即结构与最大类型的对齐要求对齐。
这些结构是填充的还是包装好的?
他们是软垫的。
最初想到的唯一可能是,如果 char
和 int
大小相同,那么 char/int/char
结构的最小大小将允许没有填充,int/char
结构同上。
但是,这需要 sizeof(int)
和 sizeof(char)
都为 4(以获得 12 和 8 个尺寸)。整个理论分崩离析,因为它由标准保证 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");
如果这些结构中有任何填充,这样的东西将拒绝编译。
仅当您明确告诉编译器打包结构时,才完成结构打包。填充是您所看到的。您的 32 位系统正在将每个字段填充到字对齐。如果您告诉编译器打包结构,它们将分别为 6 和 5 个字节。不要那样做。它不是可移植的,并且会使编译器生成更慢(有时甚至是错误)的代码。
数据结构对齐是数据在计算机内存中排列和访问的方式。它由两个独立但相关的问题组成:数据对齐和data structure padding。当现代计算机读取或写入内存地址时,它将以字大小的块(例如 32 位系统上的 4 字节块)或更大的形式执行此操作。数据对齐意味着将数据放置在等于字大小的某个倍数的内存地址上,由于 CPU 处理内存的方式,这会提高系统的性能。为了对齐数据,可能需要在最后一个数据结构的结尾和下一个数据结构的开头之间插入一些无意义的字节,这就是数据结构填充。
为了对齐内存中的数据,在分配内存时为其他结构成员分配的内存地址之间插入(或留空)一个或多个空字节(地址)。这个概念称为结构填充。计算机处理器的架构是这样一种方式,它可以一次从内存中读取 1 个字(32 位处理器中的 4 个字节)。为了利用处理器的这一优势,数据总是被对齐为 4 字节的包,这导致在其他成员的地址之间插入空地址。由于 C 中的结构填充概念,结构的大小总是与我们想象的不同。
不定期副业成功案例分享