结构填充物和填料

考虑:

struct mystruct_A
{
char a;
int b;
char c;
} x;


struct mystruct_B
{
int b;
char a;
} y;

结构尺寸分别为12和8。

这些结构是填充的还是包装的?

什么时候进行填充或包装?

424974 次浏览

填充和填充只是同一事物的两个方面:

  • 包装或对齐是每个成员四舍五入的大小
  • 填充是为匹配对齐而添加的额外空间

mystruct_A中,假设默认对齐为4,每个成员都以4字节的倍数进行对齐。由于char的大小为1,因此ac的填充为4 - 1 = 3字节,而int b则不需要填充,因为它已经是4个字节。它对mystruct_B的工作方式相同。

只有当你告诉编译器显式地对结构进行打包时,才会进行结构打包。你看到的是填充。您的32位系统正在填充每个字段以字对齐。如果您告诉编译器打包结构,它们将分别为6和5字节。但是不要这样做。它不可移植,使编译器生成的代码更慢(有时甚至有bug)。

填充 对齐结构成员用于“自然”寻址边界——例如,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大小的结构。

不过需要注意的是,在允许unaligned内存访问的体系结构(如x86和amd64)上,unaligned内存访问速度较慢,并且在严格的对齐架构上是明确禁止的,如SPARC。

结构填充抑制结构填充,当对齐最重要时使用填充,当空间最重要时使用填充。

一些编译器提供#pragma来抑制填充或将其打包为n个字节。有些公司提供关键字来做到这一点。通常,用于修改结构填充的pragma将采用以下格式(取决于编译器):

#pragma pack(n)

例如,ARM提供了__packed关键字来抑制结构填充。查阅编译器手册了解更多相关信息。

所以一个填充结构是一个没有填充的结构。

一般采用填料结构

  • 为了节省空间

  • 格式化一个数据结构,以便使用some在网络上传输 协议(当然,这不是一个好的实践,因为您需要
    处理字节顺序)

数据结构对齐是在计算机内存中排列和访问数据的方式。它由两个独立但相关的问题组成:数据对齐和数据结构填充。当现代计算机读取或写入内存地址时,它将以字大小的块(例如32位系统上的4字节块)或更大的块进行。数据对齐意味着将数据放在一个等于字大小的倍数的内存地址上,由于CPU处理内存的方式,这会提高系统的性能。为了对齐数据,可能需要在上一个数据结构的末尾和下一个数据结构的开头之间插入一些无意义的字节,这就是数据结构填充。

  1. 为了对齐内存中的数据,在内存分配时,在分配给其他结构成员的内存地址之间插入一个或多个空字节(地址)(或留空)。这个概念叫做结构填充。
  2. 计算机处理器的结构是这样的,它可以一次从内存中读取1个字(在32位处理器中是4个字节)。
  3. 为了利用处理器的这一优势,数据总是对齐为4字节的包,这导致在其他成员的地址之间插入空地址。
  4. 由于C语言中的结构填充概念,结构的大小总是与我们想象的不一样。

(上面的答案解释的很清楚,但似乎不完全清楚填充的大小,所以,我将根据我所学到的The Lost Art of Structure Packing,它已经发展到不局限于__ABC0,但也适用于__ABC1, Rust)


内存对齐(用于struct)

规则:

    在每个成员之前,将有填充,使其从可被其对齐要求整除的地址开始。
    例如,在许多系统中,int应以能被4整除的地址开始,而short应以能被2整除的地址开始
  • charchar[]是特殊的,可以是任何内存地址,所以它们之前不需要填充。
  • 对于struct,除了每个单独成员的对齐要求外,整个结构本身的大小将被对齐为可被其任何成员的最严格对齐要求整除的大小,即在结束时通过填充。
    例如,在许多系统中,如果struct的最大成员是int,则可被4整除,如果short则可被2整除

会员顺序:

    成员的顺序可能会影响结构的实际大小,所以要记住这一点。 例如,下面例子中的stu_cstu_d具有相同的成员,但顺序不同,并且导致两个结构体的大小不同

内存中的地址(用于struct)

空的空间:

  • 两个结构之间的空白可以被适合的非结构变量使用。
    例如,在下面的test_struct_address()中,变量x位于相邻的结构gh之间。
    无论是否声明xh的地址都不会改变,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

enter image description here

我知道这个问题很老了,这里的大多数答案都很好地解释了填充,但当我自己试图理解它时,我发现对正在发生的事情有一个“视觉”形象是有帮助的。

处理器以一定大小(字)的“块”读取内存。假设处理器字有8字节长。它将把内存看作一个8字节的大行构建块。每当它需要从内存中获取一些信息时,它就会到达其中一个块并获取它。

Variables Alignment

如上图所示,一个Char(1字节长)在哪里并不重要,因为它将在其中一个块中,只需要CPU处理1个字。

当我们处理大于一个字节的数据时,比如4字节的int或8字节的double,它们在内存中的对齐方式会对CPU必须处理多少字产生影响。如果4字节块以某种方式对齐,它们总是适合块的内部(内存地址是4的倍数),只有一个字将必须被处理。否则,一个4字节的数据块可能一部分在一个块上,另一部分在另一个块上,这就需要处理器处理2个字来读取这个数据。

这同样适用于8字节的double,只不过现在它必须在8的倍数内存地址中,以确保它始终在块中。

这里考虑的是8字节的字处理器,但这个概念也适用于其他大小的字。

填充通过填充这些数据之间的间隙来确保它们与这些块对齐,从而提高读取内存时的性能。

然而,正如其他人回答的那样,有时空间比性能本身更重要。也许您正在一台没有太多RAM的计算机上处理大量数据(可以使用交换空间,但速度要慢得多)。你可以在程序中排列变量,直到完成最少的填充(这在其他一些答案中得到了很好的例子),但如果这还不够,你可以显式地禁用填充,这就是包装

想要掌握这门学科的人必须做到以下几点:

  • 阅读由Eric S. Raymond撰写的《失落的结构包装艺术
  • 浏览Eric的代码示例
  • 最后但并非最不重要的是,不要忘记下面关于填充的规则:结构体是按最大类型的对齐方式对齐的 李要求。< /强> < / >

填充规则:

  1. 结构体的每个成员的地址都应该能被其大小整除。 填充在元素之间或结构的末尾插入,以确保满足此规则。这样做是为了硬件更容易和更有效地访问总线
  2. 结构体末尾的填充是根据结构体最大成员的大小决定的。

为什么 考虑下面的结构体

Struct 1

如果我们要为这个struct创建一个数组(包含2个struct), 结束时不需要填充:

Struct1 array

因此,struct的大小= 8字节

假设我们要创建另一个结构体,如下所示:

Struct 2

如果我们要创建这个结构体的数组, 结尾需要填充的字节数有两种可能

A.如果我们在末尾添加3个字节,并将其对齐为int而不是Long:

Struct2 array aligned to int

B.如果我们在末尾添加7个字节并将其对齐为Long:

Struct2 array aligned to Long

第二个数组的起始地址是8(i. 0)的倍数。e 24)。 struct的大小= 24 bytes

因此,通过将结构体的下一个数组的起始地址对齐为最大成员(i。E如果我们要创建这个结构体的数组,第二个数组的第一个地址必须从一个地址开始,该地址必须是该结构体最大成员的倍数。这里是24(3 * 8)),我们可以计算出最后所需的填充字节数。

这些结构是填充的还是包装的?

它们填充。

最初能想到的唯一可能是,如果charint是相同的大小,那么char/int/char结构的最小大小将允许没有填充,int/char结构也是如此。

然而,这将要求sizeof(int)sizeof(char)都为四个(以获得12和8的大小)。由于sizeof(char)总是 1这一标准保证了整个理论的崩溃。

如果charint宽度相同,则大小为1和1,为4和4。因此,为了得到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");

如果这些结构中有任何填充,类似这样的东西将拒绝编译。

变量存储在可以被其对齐方式(通常是大小)整除的任何地址上。所以,填充/填充不仅仅是为了结构。实际上,所有数据都有自己的对齐要求:

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)为了正确地从struct的地址启动下一个struct实例,将一些字节追加到每个struct:

// 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.
};
  1. 结构体的第一个成员总是从可以被结构体自身的对齐要求整除的任何地址开始,这是由最大成员的对齐要求决定的(这里是4int32_t的对齐)。这与正常变量不同。普通变量可以开始任何可以被其对齐方式整除的地址,但struct的第一个成员却不是这样。如您所知,结构体的地址与其第一个成员的地址相同。
  2. 在结构体内部可以有附加的填充尾随字节,使下一个结构体(或结构体数组中的下一个元素)从它自己的地址开始。想想struct st arr[2];。为了使arr[1](arr[1]的第一个成员)从一个能被4整除的地址开始,我们应该在每个结构体的末尾追加3个字节。

这是我从《失落的结构包装艺术中学到的。

注意:你可以通过_Alignof操作符来调查数据类型的对齐要求。同样,你也可以通过offsetof宏获取结构中成员的偏移量。