结构成员对齐、填充和数据打包是什么意思?

2021年4月19日12:12:03 发表评论 926 次浏览

数据对齐, 结构打包和填充是什么意思?

预测以下程序的输出。

#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) = %d\n" , sizeof (structa_t));
    printf ( "sizeof(structb_t) = %d\n" , sizeof (structb_t));
    printf ( "sizeof(structc_t) = %d\n" , sizeof (structc_t));
    printf ( "sizeof(structd_t) = %d\n" , sizeof (structd_t));
  
    return 0;
}

在继续之前, 请在纸上写下答案, 然后继续阅读。如果你希望看到解释, 你可能会想了解类比中的任何不足之处。数据对齐:

C/C++中的每种数据类型都有对齐要求(实际上, 它是由处理器体系结构而不是语言来强制要求的)。处理器将具有与数据总线大小相同的处理字长。在32位计算机上, 处理字的大小为4个字节。

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

从历史上看, 存储器是字节可寻址的, 并按顺序排列。如果将存储器安排为一个字节宽度的单个存储体, 则处理器需要发出4个存储器读取周期来获取整数。在一个内存周期中读取整数的所有4个字节更为经济。为了利用这种优势, 如上图所示, 存储器将按4个存储体的组进行排列。

内存寻址仍然是顺序的。如果存储区0占用地址X, 则存储区1, 存储区2和存储区3将位于(X + 1), (X + 2)和(X + 3)地址。如果在X地址上分配了4个字节的整数(X是4的倍数), 则处理器仅需要一个存储周期即可读取整个整数。

反之, 如果整数是在非4的倍数的地址处分配的, 则它跨越存储体的两行, 如下图所示。这样的整数需要两个内存读取周期才能读取数据。

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

变量的数据对齐处理存储在这些存储库中的数据的方式。例如, 自然对齐int在32位计算机上为4字节。自然对齐数据类型后, CPU会以最少的读取周期来获取数据。

同样, 短整数是2个字节。意思是短整数可以存储在bank 0 – bank 1对或bank 2 – bank 3对中。一种双需要8个字节, 并且在存储体中占用两行。的任何未对准双将强制进行两个以上的读取周期双数据。

请注意双变量将在32位计算机上的8字节边界上分配, 并且需要两个内存读取周期。在64位计算机上, 根据存储体数量, 双变量将在8字节边界上分配, 并且只需要一个内存读取周期。

结构填充

在C/C++中, 结构用作数据包。它不提供任何数据封装或数据隐藏功能(C++情况是例外, 因为它与类的语义相似)。

由于各种数据类型的对齐要求, 因此结构的每个成员都应自然对齐。结构的成员依次分配升序。让我们分析以上程序中声明的每个结构。

以上程序输出:

为了方便起见, 假设每个结构类型变量都分配在4个字节的边界上(例如0x0000), 即结构的基址是4的倍数(不一定需要, 请参见structc_t的说明)。

结构A

structa_t第一个元素是char这是一个字节对齐, 后跟短整数。 short int是2字节对齐的。如果short int元素在char元素之后立即分配, 则它将从奇数地址边界开始。编译器将在char之后插入一个填充字节, 以确保short int的地址倍数为2(即2字节对齐)。 structa_t的总大小将为sizeof(char)+ 1(填充)+ 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个字节, 而不是24个字节的structc_t。

什么是结构包装?

有时必须避免在结构成员之间填充字节。例如, 读取ELF文件头或BMP或JPEG文件头的内容。我们需要定义一个与标题布局相似的结构并将其映射。但是, 在访问此类成员时应格外小心。通常, 逐字节读取是避免未对齐异常的一种选择。性能会受到影响。

大多数编译器提供非标准扩展来关闭默认填充, 例如编译指示或命令行开关。有关更多详细信息, 请查阅相应编译器的文档。

指针错误:

处理指针算术时可能会出现错误。例如, 如下所示取消引用通用指针(void *)会导致未对齐的异常,

//Deferencing a generic pointer (not safe)
//There is no guarantee that pGeneric is integer aligned
*(int *)pGeneric;

编程中可能存在以上代码类型。如果指针pGeneric根据转换的数据类型的要求未对齐, 则可能会出现未对齐的异常。

实际上, 很少有处理器没有地址解码的最后两位, 并且无法访问错位地址。如果程序员尝试访问该地址, 则处理器会产生未对齐的异常。

关于malloc()返回的指针的说明

malloc()返回的指针是无效*。根据程序员的需要, 可以将其转换为任何数据类型。 malloc()的实现者应返回一个与原始数据类型(由编译器定义)的最大大小对齐的指针。通常在32位计算机上与8字节边界对齐。

目标文件对齐, 节对齐, 页面对齐

这些特定于操作系统实现者, 编译器作者, 不在本文的讨论范围之内。实际上, 我没有太多信息。

一般的问题:

1.对齐是否应用于堆栈?

是。堆栈也是内存。系统程序员应使用正确对齐的内存地址加载堆栈指针。通常, 处理器不会检查堆栈对齐, 程序员有责任确保堆栈内存正确对齐。任何未对准都会导致运行时意外。

例如, 如果处理器字长为32位, 则堆栈指针也应对齐为4个字节的倍数。

2.如果char数据放置在其他存储区0的存储区中, 则在读取存储器时会将其放置在错误的数据线上。处理器如何处理char类型?

通常, 处理器会根据指令识别数据类型(例如, ARM处理器上的LDRB)。根据存储的存储区, 处理器将字节移到最低有效数据线上。

3.当参数在堆栈上传递时, 它们是否要对齐?

是。编译器帮助程序员进行正确的对齐。例如, 如果将16位值压入32位宽的堆栈, 则该值将自动用0填充到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位长度强制将double类型分配在8个字节的边界上。我假设(我没有具体信息)在FPU操作的情况下, 数据获取可能有所不同, 我的意思是数据总线, 因为它通过FPU。因此, 对于双精度类型(预期在8字节边界上), 地址解码将有所不同。它的意思是, 浮点单元的地址解码电路将没有最后3个引脚.

答案:

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

更新:2013年5月1日

据观察, 在最新的处理器上, 我们将struct_c的大小设为16个字节。我尚未阅读相关文档。一旦获得适当的信息(写给少数硬件专家), 我将进行更新。

在使用相同工具集(GCC++4.7)的较旧处理器(AMD Athlon X2)上, 我得到的struct_c大小为24字节。大小取决于在硬件级别组织内存存储的方式。

– – –由文基。如果发现任何不正确的地方, 或者想分享有关上述主题的更多信息, 请写评论。

木子山

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: