结构填充和包装


209

考虑:

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

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

结构的大小分别为12和8。

这些结构是被填充还是包装好?

填充或包装何时进行?



24
C结构打包的失落艺术-catb.org/esr/structure-packing
Paolo,

padding使事情变得更大。packing使事情变得更小。完全不同。
smwikipedia

Answers:


264

填充 结构成员与“自然”地址边界对齐 -例如,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阻止编译器进行填充(必须明确请求填充),在GCC下,它是__attribute__((__packed__)),因此:

struct __attribute__((__packed__)) mystruct_A {
    char a;
    int b;
    char c;
};

6在32位架构上生成大小结构。

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


2
我想知道:禁止在火花上对齐内存意味着它不能处理通常的字节数组吗?据我所知,当您需要将字节数组转换为结构时,并确保数组适合结构字段时,结构打包主要用于传输(即联网)数据。如果火花不能做到这一点,那么这些将如何工作?
Hi-Angel

14
这就是为什么,如果查看IP,UDP和TCP标头布局,就会发现所有整数字段都对齐。
Nikolai Fetissov

17
所谓“C结构包装的失落艺术”解释填充和包装ptimisations - catb.org/esr/structure-packing
Rob11311

3
第一位成员必须先到吗?我认为arrage完全取决于实现,因此不能依赖(即使版本之间也是如此)。
allyourcode

4
+ allyourcode该标准保证将保留成员的顺序,并且第一个成员将从0偏移量开始。
martinkunev

64

上面的答案很清楚地解释了原因,但是关于填充的大小似乎并不十分清楚,因此,我将根据我从“结构包装的失落的艺术”中学到知识来添加一个答案,它已经演变到不仅限于C。也适用于GoRust


内存对齐(针对结构)

规则:

  • 在每个单独的成员之前,都会有填充,以便使其从可被其大小整除的地址开始。
    例如在64位的系统,int应该通过在4整除地址开始,并long通过如图8所示,short由2。
  • char并且char[]很特殊,可以是任何内存地址,因此它们之前不需要填充。
  • 对于struct,除了每个单独成员的对齐需要之外,整个结构本身的大小将通过末端填充而对齐为可被最大单个成员的大小整除的大小。
    例如,如果long然后将struct的最大成员除以8,int然后除以4,short然后乘以2。

会员顺序:

  • 成员的顺序可能会影响struct的实际大小,因此请记住这一点。例如,下面的stu_cand stu_dfrom实例具有相同的成员,但是顺序不同,并且导致2个结构的大小不同。

内存中的地址(用于结构)

规则:

  • 64位系统
    结构地址从(n * 16)字节开始。(您可以在下面的示例中看到,所有打印的结构十六进制地址都以0。结尾。
    原因:可能的最大单个结构成员为16个字节(long double)。
  • (更新)如果结构仅包含charas成员,则其地址可以从任何地址开始。

可用空间

  • 可能适合的非结构变量可以使用2个结构之间的空白。
    例如,在test_struct_address()下面,该变量x位于相邻的struct 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

在此处输入图片说明


4
“规则”实际上很明确,我在任何地方都找不到简单的规则。谢谢。
Pervez Alam

2
@PervezAlam这本书<The Lost Art of C Structure Packing>很好地解释了规则,甚至认为它比这个答案要长一点。该书可在线免费获得:catb.org/esr/structure-packing
Eric Wang

我会尝试一下,顺便说一句,它仅限于结构包装吗?我喜欢书中的解释,只是古玩。
Pervez Alam

1
@PervezAlam这是一本非常简短的书,主要关注减少c程序内存占用的技术,最多只需要几天就能完成阅读。
埃里克·王

1
@ValidusOculus是的,这意味着16字节对齐。
Eric Wang

44

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

处理器以一定大小(字)的“块”形式读取内存。假设处理器字长为8个字节。它将内存看成是8字节构建块的大行。每当它需要从内存中获取一些信息时,它将到达这些块之一并获取它。

变量对齐

如上图所示,Char(1个字节长)在哪里都无所谓,因为它将位于这些块之一中,因此CPU仅需要处理1个字。

当我们处理大于一个字节的数据时,例如4字节的int或8字节的double,它们在内存中的对齐方式将影响CPU必须处理多少个字。如果将4字节的块对齐,使其始终适合块的内部(内存地址为4的倍数),则仅需处理一个字。否则,一块4字节的块可能一部分在一个块上,另一部分在另一个块上,这要求处理器处理2个字来读取此数据。

同样适用于8字节的double,但是现在它必须位于8的内存地址中,以确保它始终位于一个块内。

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

填充通过填充这些数据之间的间隙以确保它们与这些块对齐来起作用,从而在读取存储器时提高了性能。

但是,正如其他答案所述,有时空间比性能本身更重要。也许您正在没有太多RAM的计算机上处​​理大量数据(可以使用交换空间,但速度要慢得多)。您可以在程序中排列变量,直到完成最少的填充为止(这在其他一些答案中得到了很好的说明),但是,如果这还不够的话,您可以显式禁用填充(即打包)。


3
这不能解释结构压缩,但是可以很好地说明CPU字对齐。
David Foerster

你是用油漆画的吗?:-)
Ciro Santilli郝海东冠状病六四事件法轮功

1
@ CiroSantilli709大抓捕六四事件法轮功,虽然有点g,但是我想我已经节省了一些时间在油漆上做这件事
IanC

1
自开源以来,效果更好(Y)
Ciro Santilli郝海东冠状病六四事件法轮功

21

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

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

#pragma pack(n)

例如,ARM提供了__packed关键字来抑制结构填充。仔细阅读您的编译器手册,以了解有关此内容的更多信息。

因此,打包结构是没有填充的结构。

通常使用包装结构

  • 节省空间

  • 使用某种协议格式化数据结构以通过网络传输(这当然不是一个好习惯,因为您需要
    处理字节序)


5

填充和打包只是同一件事的两个方面:

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

在中mystruct_A,假定默认对齐方式为4,则每个成员都按4字节的倍数对齐。由于的大小char为1,因此a和的填充为4-1 c= 3字节,而无需填充int b已经为4字节。的工作方式相同mystruct_B


1

仅当您明确告诉编译器打包结构时,才进行结构打包。填充就是您所看到的。您的32位系统会将每个字段填充到单词对齐位置。如果您告诉编译器打包结构,则它们分别为6和5个字节。不过不要那样做。它不是可移植的,使编译器生成的代码慢得多(有时甚至是错误的)。


1

有没有关于它!想要掌握主题的人必须做以下事情,


1

填充规则:

  1. 结构的每个成员都应位于可按其大小整除的地址。在元素之间或结构的末尾插入了填充,以确保满足此规则。这样做是为了使硬件更轻松,更有效地访问总线。
  2. 根据结构中最大成员的大小确定结构末尾的填充。

为什么要使用规则2:请考虑以下结构,

结构1

如果我们要创建此结构的数组(包含2个结构),则最后不需要填充:

Struct1数组

因此,struct的大小= 8个字节

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

结构2

如果要创建此结构的数组,则有两种可能性,即最后所需的填充字节数。

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

Struct2数组与int对齐

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

Struct2数组与Long对齐

第二个数组的起始地址是8的倍数(即24)。结构的大小= 24字节

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


-1

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

  1. 为了对齐内存中的数据,在分配内存时为其他结构成员分配的内存地址之间插入一个或多个空字节(地址)(或使其为空)。这个概念称为结构填充。
  2. 计算机处理器的体系结构可以一次从内存中读取1个字(32位处理器中为4字节)。
  3. 为了利用处理器的这一优势,数据始终按4字节的包对齐,这导致在其他成员的地址之间插入空地址。
  4. 由于在C语言中使用了这种结构填充概念,因此结构的大小始终与我们认为的不同。

1
为什么您的答案中需要链接至同一文章5次?请仅保留该示例的一个链接。另外,由于您要链接到文章,因此您需要披露该事实。
Artjom B.
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.