1. 主页
  2. 文档
  3. C语言教程
  4. C语言枚举、结构和联合
  5. 结构成员对齐、填充和数据打包

结构成员对齐、填充和数据打包

数据对齐、结构打包和填充是什么意思?
预测以下程序的输出。


#include <stdio.h>

// Alignment requirements
// (typical 32 bit machine)

// char 1 byte
// short int 2 bytes
// int 4 bytes
// double 8 bytes

// structure A
typedef struct structa_tag
{
char c;
short int s;
} structa_t;

// structure B
typedef struct structb_tag
{
short int s;
char c;
int i;
} structb_t;

// structure C
typedef struct structc_tag
{
char c;
double d;
int s;
} structc_t;

// structure D
typedef struct structd_tag
{
double d;
int s;
char c;
} structd_t;

int main()
{
printf("sizeof(structa_t) = %lu\n", sizeof(structa_t));
printf("sizeof(structb_t) = %lu\n", sizeof(structb_t));
printf("sizeof(structc_t) = %lu\n", sizeof(structc_t));
printf("sizeof(structd_t) = %lu\n", sizeof(structd_t));

return 0;
}

在继续之前,在纸上写下你的答案,然后继续阅读。如果您急于查看解释,您可能会错过理解类比中的任何漏洞。数据对齐:
C/C++ 中的每种数据类型都会有对齐要求(实际上它是由处理器架构而非语言强制要求的)。处理器将具有与数据总线大小一样的处理字长。在 32 位机器上,处理字大小为 4 个字节。

从历史上看,内存是按字节寻址并按顺序排列的。如果内存被安排为一个字节宽度的单个 bank,处理器需要发出 4 个内存读取周期来获取一个整数。在一个内存周期中读取所有 4 个字节的整数更经济。为了利用这一优势,存储器将按上图所示的 4 个组排列。
内存寻址仍然是顺序的。如果 bank 0 占用地址 X,bank 1、bank 2 和 bank 3 将位于 (X + 1)、(X + 2) 和 (X + 3) 地址。如果在 X 地址上分配一个 4 字节的整数(X 是 4 的倍数),处理器只需要一个内存周期即可读取整个整数。
其中,如果整数分配在 4 的倍数以外的地址,则它跨越两行存储库,如下图所示。这样一个整数需要两个内存读取周期来获取数据。

变量的 数据对齐处理数据存储在这些银行中的方式。例如int在 32 位机器上的自然对齐是 4 个字节。当数据类型自然对齐时,CPU 会在最短的读取周期内获取它。
同样,short int的自然对齐方式是 2 个字节。这意味着,一个短整数可以存储在 bank 0 – bank 1 pair 或 bank 2 – bank 3 pair 中。一个double需要 8 个字节,并在内存条中占据两行。double的任何未对齐都会强制两个以上的读取周期来获取double数据。
请注意, 变量将在 32 位机器上分配在 8 字节边界上,并且需要两个内存读取周期。在 64 位机器上,根据存储体的数量,变量将分配在 8 字节边界上,并且只需要一个内存读取周期。
结构填充:
在 C/C++ 中,结构用作数据包。它不提供任何数据封装或数据隐藏功能(C++ 案例是一个例外,因为它与类的语义相似)。
由于各种数据类型的对齐要求,结构的每个成员都应该自然对齐。结构的成员按递增顺序分配。让我们分析上面程序中声明的每个结构。
上述程序的输出:
为方便起见,假设每个结构类型变量都分配在 4 字节边界上(例如 0x0000),即结构的基地址是 4 的倍数(不一定总是需要,参见 structc_t 的说明)。
结构 A structa_t
第一个元素是char,它是一个字节对齐的,然后是short int。short int 是 2 字节对齐的。如果在 char 元素之后立即分配 short int 元素,它将从奇数地址边界开始。编译器将在 char 之后插入一个填充字节,以确保 short int 具有 2 的地址倍数(即 2 字节对齐)。structa_t 的总大小将为 sizeof(char) + 1 (padding) + sizeof(short),1 + 1 + 2 = 4 个字节。
结构B
structb_t的第一个成员是 short int,后跟 char。由于 char 可以位于任何字节边界上,因此在 short int 和 char 之间不需要填充,因此它们总共占用 3 个字节。下一个成员是 int。如果立即分配 int,它将从奇数字节边界开始。我们需要在 char 成员之后填充 1 个字节,以使下一个 int 成员的地址是 4 字节对齐的。上总的structb_t需要2 + 1 + 1(填充)+ 4 = 8个字节。
结构 C – 每个结构也将有对齐要求
应用相同的分析,structc_t需要 sizeof(char) + 7 字节填充 + sizeof(double) + sizeof(int) = 1 + 7 + 8 + 4 = 20 字节。但是,sizeof(structc_t) 将是 24 个字节。这是因为,与结构成员一起,结构类型变量也会自然对齐。让我们通过一个例子来理解它。比如说,我们声明了一个 structc_t 数组,如下所示 

structc_t structc_array[3];

假设structc_array的基地址为 0x0000 以便于计算。如果我们计算的 structc_t 占用 20 (0x14) 个字节,则第二个 structc_t 数组元素(索引为 1)将位于 0x0000 + 0x0014 = 0x0014。它是数组索引 1 元素的起始地址。此 structc_t 的 double 成员将分配在 0x0014 + 0x1 + 0x7 = 0x001C(十进制 28)上,这不是 8 的倍数,并且与 double 的对齐要求相冲突。正如我们在顶部提到的,double 的对齐要求是 8 个字节。
为了避免这种错位,编译器将对每个结构引入对齐要求。它将是该结构中最大的成员。在我们的例子中,structa_t 的对齐是 2,structb_t 是 4,structc_t 是 8。如果我们需要嵌套结构,最大内部结构的大小将是直接较大结构的对齐。
在上述程序的structc_t中,在int成员后面会有4个字节的填充,使结构体大小是其对齐的倍数。因此 sizeof (structc_t) 是 24 字节。即使在数组中,它也能保证正确对齐。你可以交叉检查。
结构 D – 如何减少填充?
到目前为止,很明显填充是不可避免的。有一种方法可以最小化填充。程序员应该按照大小的递增/递减顺序声明结构成员。一个例子是我们代码中给出的 structd_t,它的大小是 16 字节,而不是 structc_t 的 24 字节。
什么是结构填料?
有时必须避免结构成员之间的填充字节。例如,读取 ELF 文件头或 BMP 或 JPEG 文件头的内容。我们需要定义一个类似于标题布局的结构并映射它。但是,在访问此类成员时应小心谨慎。通常,逐字节读取是避免未对齐异常的一种选择。性能会受到打击。
大多数编译器提供非标准扩展来关闭默认填充,如编译指示或命令行开关。有关更多详细信息,请参阅相应编译器的文档。
指针事故:
处理指针算术时可能会出错。例如,如下所示取消引用通用指针 (void *) 可能会导致未对齐异常, 
 

// 取消引用泛型指针(不安全)
// 不能保证 pGeneric 是整数对齐的
*(int *)pGeneric;

在编程中可以使用上述类型的代码。如果指针pGeneric未按照转换数据类型的要求对齐,则可能会出现未对齐异常。
事实上,很少有处理器没有地址解码的最后两位,并且没有办法访问未对齐的地址。如果程序员试图访问这样的地址,处理器会产生未对齐的异常。
关于 malloc() 返回指针
说明 malloc() 返回的指针是void *. 它可以根据程序员的需要转换为任何数据类型。malloc() 的实现者应该返回一个与原始数据类型(由编译器定义的那些)的最大大小对齐的指针。它通常与 32 位机器上的 8 字节边界对齐。
目标文件对齐、节对齐、页面对齐
这些是特定于操作系统实现者、编译器编写者的,超出了本文的范围。事实上,我没有太多信息。
一般问题:
1. 对齐是否适用于堆栈?
是的。栈也是内存。系统程序员应该使用正确对齐的内存地址加载堆栈指针。通常,处理器不会检查堆栈对齐,程序员有责任确保堆栈内存的正确对齐。任何错位都会导致运行时出现意外。
例如,如果处理器字长为 32 位,堆栈指针也应对齐为 4 字节的倍数。
2. 如果char数据放在bank other bank 0中,在内存读取时会被放在错误的数据线上。处理器如何处理 char类型?
通常,处理器会根据指令识别数据类型(例如 ARM 处理器上的 LDRB)。根据存储的组,处理器将字节转移到最不重要的数据线上。
3. 当参数在堆栈上传递时,它们是否受到对齐?
是的。编译器帮助程序员进行正确的对齐。例如,如果一个 16 位的值被压入一个 32 位宽的堆栈,该值会自动用零填充到 32 位。考虑以下程序。

void argument_alignment_check( char c1, char c2 )
{
// Considering downward stack
// (on upward stack the output will be negative)
printf("Displacement %d\n", (int)&c2 - (int)&c1);
}

在 32 位机器上输出将是 4。这是因为对齐要求,每个字符占用 4 个字节。
4. 如果我们尝试访问未对齐的数据会发生什么?
这取决于处理器架构。如果访问未对齐,处理器会自动发出足够的内存读取周期并将数据正确打包到数据总线上。惩罚是在性能上。很少有处理器没有最后两条地址线,这意味着无法访问奇数字节边界。每个数据访问都必须正确对齐(4 个字节)。未对齐的访问是此类处理器的关键异常。如果忽略异常,读取的数据将不正确,从而导致结果。
5.有什么方法可以查询一个数据类型的对齐要求。
是的。编译器为此类需求提供了非标准扩展。例如,Visual Studio 中的 __alignof() 有助于获取数据类型的对齐要求。阅读 MSDN 了解详细信息。
6. 当内存读取在 32 位机器上一次读取 4 个字节时是有效的,为什么双精度类型要在 8 字节边界上对齐?
需要注意的是,大多数处理器都有数学协处理器,称为浮点单元 (FPU)。代码中的任何浮点运算都会被翻译成 FPU 指令。主处理器与浮点执行无关。所有这些都将在幕后完成。
按照标准,双精度类型将占用 8 个字节。而且,在 FPU 中执行的每个浮点运算都是 64 位长度的。甚至浮点类型也会在执行前提升为 64 位。
FPU 寄存器的 64 位长度强制在 8 字节边界上分配双精度类型。我假设(我没有具体信息)在 FPU 操作的情况下,数据获取可能不同,我的意思是数据总线,因为它进入 FPU。因此,双精度类型的地址解码将不同(预计在 8 字节边界上)。这意味着,浮点单元的地址解码电路将没有最后 3 个引脚
答案: 

sizeof(structa_t) = 4
sizeof(structb_t) = 8
sizeof(structc_t) = 24
sizeof(structd_t) = 16

 

这篇文章对您有用吗?