结构填充

Structure Padding

我知道编译器会进行结构填充,因为 CPU 一次可以读取一个单词(单词大小取决于体系结构 16/32/64)但我无法理解以下示例的填充行为。

struct pad 
{ 
  char ch;
};

"struct pad" 的大小为 1 字节(但根据填充概念我怀疑是 4 字节(32 位架构)。

这里,struct实际上有1B大小,这里不需要填充。

实际结构填充如下:

#include <stdio.h>
struct char_int {
        char a;
        // 3B padding here.
        int b;
};

int main() {
        struct char_int struct_a;
        struct_a.a = 0;
        struct_a.b = 12;
        printf("sizeof(struct_a):%d, &struct_a.a:0x%x, &struct_a.b:0x%x\n",
                        sizeof(struct char_int), &struct_a.a, &struct_a.b);
        return 0;
}

在我的环境中,GCC 4.9.2 RHEL AMD64 5.9。输出如下:

  sizeof(struct_a):8, &struct_a.a:0x36a41fd0, &struct_a.b:0x36a41fd4

在栈或堆中为上述结构分配内存时,char aint b之间会填充3个字节,因为这里字段char_int.b有地址偏移量4,而文件 char_int.a 只有 1 个字节。

char 类型可以在任何地址边界上高效访问,因此不需要填充。

已定义结构的大小为 1 个字节,此处未使用填充。因为char占用1字节内存,没有其他数据类型。

struct s1
{
  char a;
  char b;
}

此 returns 结构大小为 2。当使用不同大小的变量时,结构填充是为了更容易和更快地处理地址。

您混淆了内存填充和内存控制器寻址(通常对字进行操作)的概念。

出于各种原因,C 强制执行标准结构对齐要求(尽管通常可以使用编译器 #pragmas 覆盖这些要求)。按照标准,你定义的struct确实应该有1个字节的大小。

然而,当在 x86 CPU 上执行此代码时,内存控制器将加载 4 字节对齐的字,并且 CPU 将为结构提取 1 字节的值。无论结构是否为 4 字节对齐,都会发生这种情况,并且对性能没有影响。

首先,它是编译器,而不是添加填充的 CPU — 但是编译器会根据特定目标 CPU 功能(主要是其数据总线宽度)添加填充。

添加了填充字节以提高内存访问效率。当 CPU 通过 16 位总线在偶地址上读取或写入 2 字节字时,传输可以在一个周期内完成。但是,如果 2 字节数据位于奇地址,它会占用内存中一个偶地址字的 'lower' 一半和另一个偶地址字的 'upper' 一半。 CPU 然后必须执行两个总线周期:一个用于数据占用的每个内存字,每次只使用一半的总线宽度进行实际传输。

对于 32 位总线,如果 2 字节或 4 字节的数据跨越双字(4 字节)边界,则它也可能需要两个周期。类似的效果发生在 64 位总线上。这就是填充的原因:编译器添加字节以将数据对齐到可以有效访问它们的边界。

如果较短的成员后跟较长的成员,则可能会发生填充。

仅包含 1 字节成员的结构

struct BBB {
    byte  b1;
    byte  b2;
    byte  b3;
}

不需要任何填充 - 每个 byte 都可以在一个周期内读取或写入。
类似

struct III {
    int   i1;
    int   i2;
    int   i3;
}

只要结构与偶数边界对齐(根据其第一个成员要求),所有成员也都正确对齐。

struct BI {
    byte  b;
    // one byte added here
    int   i;
}

此处添加了一个字节,以便 2 字节 int 与 2 字节边界对齐。

struct IB {
    int   i;
    byte  b;
    // one byte added here
}

惊喜!为什么要在末尾添加额外的字节?此处需要对齐哪些数据...?
好吧,假设你声明了一个数组:

struct IB arr[4];

那么数组数据在内存中是这样排列的:

int     arr[0].i
byte    arr[0].b
// byte arr[0] padding
int     arr[1].i
byte    arr[1].b
// byte arr[1] padding
int     arr[2].i
byte    arr[2].b
// byte arr[2] padding
int     arr[3].i
byte    arr[3].b
// byte arr[3] padding

结构末尾的每个 byte 成员都得到 'extendend' 两个字节,因此下一个数组项中的 int 与偶数地址对齐。如果不填充,每个第二个数组项都会使其 int 成员未对齐。

对于 32 位总线,最多可以使用 3 个字节的填充:

struct BL {
    byte  b;
    // 3 bytes added
    long  l;
}

因此 4 字节 long 位于 4 字节边界。

结构填充是一个棘手的概念。因此,如果您不容易理解它,则无需担心。 考虑任何结构 - 如果第一个元素的大小小于结构中的下一个元素,则只需要填充,在这种情况下,应对第一个元素进行结构填充。等待 !!!还没结束。 如果你想找到整个结构的大小,还有一个 logic/concept 要应用。即结构的最终大小应能被结构中最大尺寸的元素整除。

下面举例推导

struct test {  
   char a;
   char b;
   double d;
   int m;
   double dd;
   char c;
   float f;
   double ddd;
   char e;
};

现在让我们找出每个元素的大小。

char a; 1

char b; 1

double d; 8

int m; 4

double dd; 8

char c; 1

float f; 4

double ddd; 8

char e; 8

现在我们将应用结构填充。

char a; 1 (由于上述原因无需填充)

char b; 1 + 7 (因为 char 比 double 小,填充 'double size' 的 完成)*

double d; 8 (由于上述原因无需填充)

int m; 4 + 4 (因为 int 比 double 小,'double size' 的填充完成)

double dd; 8 (由于上述原因无需填充)

char c; 1 + 3*(因为 char 比 float 小,填充 'float size' 完成)*

float f; 4 + 4 (因为 float 比 double 小,填充 'double size' 完成)

double ddd; 8 (由于上述原因无需填充)

char e; 1 (由于上述原因无需填充)

现在,如果您将所有内容相加,当您需要应用可被结构的最大元素整除的第二个概念时,这里会出现 54。 这里是 8,所以 54 + 2 = 56 可以被 8 整除。

上述结构的最终大小为 56。