结构体填充和紧凑化

324

考虑以下情况:

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

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

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

这些结构是填充还是紧凑的?

填充或者紧凑是在什么时候发生的?


3
阅读 https://dev59.com/jnVD5IYBdhLWcg3wAWoO为什么一个结构体的 sizeof 不等于每个成员变量 sizeof 的总和? - Prasoon Saurav
43
失传的C结构体打包艺术 - http://www.catb.org/esr/structure-packing/ - Paolo
3
padding 会使事物变得更大。packing 则会使事物变得更小。两者完全不同。 - smwikipedia
@Paolo,那个“Lost Art”链接没有展示指针对齐时会发生什么,以及上面两个int可能会相互挨着的情况。 - mLstudent33
相关的,针对C++:https://stackoverflow.com/questions/44287060/c-class-packing-member-alignment - Gabriel Staples
参见:https://dev59.com/2nA75IYBdhLWcg3wZ4Fr - Gabriel Staples
11个回答

382

填充 对齐 结构体成员到“自然”地址边界 - 比如,在32位平台上,int 成员将具有 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这样的严格对齐架构上明确禁止。


2
我想知道:Spark禁止使用未对齐内存,这是否意味着它无法处理常规的字节数组?据我所知,结构体打包主要用于传输(如网络),当您需要将字节数组转换为结构体并确保数组适合结构体字段时。如果Spark无法做到这一点,那么它是如何运行的?! - Hi-Angel
18
正因为如此,如果您查看IP、UDP和TCP头部布局,您会发现所有整数字段都是对齐的。 - Nikolai Fetissov
29
“失传的C结构体打包技巧”讲解了填充和打包优化——http://www.catb.org/esr/structure-packing/ - Rob11311
3
第一个成员必须排在第一位吗?我认为排列顺序完全由实现决定,不能依赖于它(即使从一个版本到另一个版本也是如此)。 - allyourcode
6
标准保证成员的顺序将被保留,并且第一个成员将从偏移量0开始。 - martinkunev
显示剩余8条评论

150

(以上答案已经很清楚地解释了原因,但似乎没有完全清楚填充的大小,因此,我将根据我从The Lost Art of Structure Packing了解到的内容添加一个答案,它已发展到不仅限于C,还适用于GoRust)


结构体内存对齐

规则:

  • 在每个成员之前,都会进行填充,以使其起始地址可以被其对齐要求整除。
    例如,在许多系统上,int 应该从可被 4 整除的地址开始,而 short 应该从可被 2 整除的地址开始。
  • charchar[] 是特殊的,可以是任何内存地址,因此它们不需要在它们之前进行填充。
  • 对于 struct,除了每个单独成员的对齐需求外,整个结构体本身的大小将通过在结尾填充以使其大小对最严格对齐要求的成员任何一个整除,
    例如,在许多系统上,如果结构体的最大成员是 int,则应该被 4 整除,如果是 short,则应该被 2 整除。

成员的顺序:

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

内存中的地址(对于结构体)

空隙

  • 两个结构体之间的空隙可以用适合其中的非结构体变量。
    例如,在下面的 test_struct_address() 中,变量 x 位于相邻结构体 gh 之间。
    无论声明了 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
“Rules” 实际上已经讲得很清楚了,我无法找到更加直接的规则。谢谢。 - Pervez Alam
3
@PervezAlam 这本书 <C结构体打包的失传艺术> 很好地解释了规则,虽然比这个回答稍微长一些。该书可以在网上免费获取:http://www.catb.org/esr/structure-packing/ - Eric
2
@PervezAlam 这是一本非常简短的书,主要关注减少 C 语言程序内存占用的技术,只需要最多几天时间就能读完。 - Eric
1
@AkshayImmanuelD 是的,每个单独的成员都遵守规则1,并且第一个成员的起始地址也是结构体本身的起始地址。 - Eric
1
@ValidusOculus 是的,它意味着16字节对齐。 - Eric
显示剩余19条评论

61

我知道这个问题已经很老了,这里的大多数答案都很好地解释了填充,但在尝试理解它时,我发现有一个“视觉”图像可以帮助。

处理器以固定大小(字)的“块状”方式读取内存。假设处理器字长为8个字节。它将查看内存作为大型的8个字节的构建块行。每次需要从内存获取一些信息时,它将到达其中一个块并获取它。

Variables Alignment

如上图所示,Char(1个字节长)位于哪里并不重要,因为它将位于这些块中的一个内部,只需要CPU处理1个字。

当我们处理比一个字节更大的数据时,例如4字节int或8字节double,它们在内存中的对齐方式会影响CPU必须处理多少个字。如果4字节块以它们总是适合块内(内存地址是4的倍数)的方式进行对齐,则只需要处理一个字。否则,4字节的块可能部分在一个块上,部分在另一个块上,需要处理2个字才能读取这些数据。

对于8字节的double,情况也是一样,只不过现在它必须位于8的倍数的内存地址中,以确保它总是在块内。

这适用于8字节字处理器,但概念适用于其他字大小。

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

然而,正如其他答案中所述,有时空间比性能更重要。也许您正在计算机上处理大量数据,而该计算机没有太多的RAM(可以使用swap space,但速度要慢得多)。您可以将程序中的变量排列到最少填充的方式(在其他答案中进行了很好的说明),但如果这还不够,可以显式禁用填充,这就是packing的作用。


5
这并没有解释结构体打包,但它很好地说明了CPU字对齐。 - David Foerster
你是在画图工具里面绘制的吗? :-) - Ciro Santilli OurBigBook.com
1
开源后更好了(Y) - Ciro Santilli OurBigBook.com
当从文件中读取结构时,这也非常方便。可以简单地读入缓冲区,然后直接使用memcpy等将其复制到结构体中。 - user3342816
您强调了一个非常重要的概念,即处理器以确定的大小(字长)读取内存。这可能是4或8,具体取决于平台架构,但如果要在纸上绘制结构/类以帮助推导其大小、编译器填充和对齐,则需要知道这一点。我几乎可以保证我们中的许多人错误地认为处理器一次读取64字节的内存(因为这是大多数现代PC上缓存行的大小),而实际上READ和LOAD之间存在差异。如果我错了,请纠正我。 - Mo Aboulmagd
你的回答中没有图片。你能更新图片或者给一个有效的链接吗? - NK-cell

26

结构体紧凑排列可以抑制结构体填充,填充用于对齐最重要的情况,紧凑排列用于空间最为重要的情况。

一些编译器提供#pragma来抑制填充或使其紧凑到n字节。有些编译器提供关键字来实现这一点。通常用于修改结构体填充的#pragma的格式如下(取决于编译器):

#pragma pack(n)

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

因此,紧凑结构是没有填充的结构。

通常,紧凑结构将被用于:

  • 节省空间

  • 使用某些协议在网络上传输数据结构的格式(当然这不是一个好的实践,因为您需要处理Endianness)


6

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

  • 对齐或者说是打包,指的是每个成员取整后的大小
  • 填充是为了与对齐相匹配而添加的额外空间

在默认对齐方式为4的情况下,mystruct_A中的每个成员都按照4字节的倍数进行对齐。由于char的大小为1,所以ac的填充为4-1=3字节,而对于已经是4字节的int b则不需要填充。mystruct_B同样也是如此。


4
变量存储在地址可被其对齐方式整除的任何位置(一般是按其大小)。因此,填充/紧凑并不仅适用于结构体。实际上,所有数据都有自己的对齐要求
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.

这类似于结构体,但有一些区别。首先,我们可以说有两种填充方式— 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.
};
  1. 结构体的第一个成员始终从结构体自身对齐要求可被整除的地址开始,该对齐要求由最大成员的对齐要求(这里是int32_t的对齐)决定。这与普通变量不同。普通变量可以从其对齐的任何地址开始,但对于结构体的第一个成员则不是这种情况。如您所知,结构体的地址与其第一个成员的地址相同。
  2. 结构体内部可能存在额外的填充字节,使得下一个结构体(或结构体数组中的下一个元素)从其自身的地址开始。考虑struct st arr[2];。为使arr [1]arr [1]的第一个成员)从可被4整除的地址开始,我们应在每个结构体的末尾附加3个字节。

这是我从The Lost Art of Structure Packing了解到的。

注意:您可以通过_Alignof运算符确定数据类型的对齐要求。此外,您可以通过offsetof宏获取结构体中成员的偏移量。


3

填充规则:

  1. 结构体的每个成员都应该在其大小可被整除的地址处。 在元素之间或结构体末尾插入填充以确保满足此规则。这是为了让硬件更容易、更有效地访问总线。
  2. 结构体末尾的填充是基于结构体中最大成员的大小来确定的。

为什么需要第二条规则:

考虑以下结构体:

Struct 1

如果我们要创建一个该结构体的数组(2个结构体),则不需要在末尾添加填充:

Struct1 array

因此,结构体大小=8个字节

现在假设我们要创建另一个结构体:

Struct 2

如果我们要创建一个该结构体的数组,有2种可能需要在末尾添加填充:

A.如果我们在末尾添加3个字节并对齐到int:

Struct2 array aligned to int

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

Struct2 array aligned to Long

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

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


2
这些结构体是填充的。
唯一可能的情况是,如果char和int大小相同,则char/int/char结构的最小大小将不允许填充,int/char结构也是如此,那么它们就可以被打包。然而,这需要sizeof(int)和sizeof(char)都为4(以获得12和8的大小)。整个理论破裂了,因为标准保证sizeof(char)始终为1。如果char和int宽度相同,则大小将为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");

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


1

结构体打包只有在您明确告诉编译器打包结构体时才会进行。所看到的是填充。您的32位系统将每个字段填充到字对齐。如果您已经告诉编译器打包结构体,它们分别为6和5个字节。不要这样做。它不具备可移植性,并且会使编译器生成更慢(有时甚至是有缺陷的)代码。


1

毫无疑问! 想要掌握这个主题的人必须完成以下步骤:


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接