数据对齐,结构打包和填充是什么意思?
预测以下程序的输出。
c
#include
// 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
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);
}
在继续之前,请在纸上写下您的答案,然后继续阅读。如果您希望看到解释,您可能会想念您的类比中的任何不足之处。数据对齐:
C / C++中的每种数据类型都有对齐要求(实际上,它是由处理器体系结构而不是语言来强制要求的)。处理器将具有与数据总线大小相同的处理字长。在32位计算机上,处理字的大小为4个字节。
从历史上看,存储器是字节可寻址的,并按顺序排列。如果将存储器安排为一个字节宽度的单个存储体,则处理器需要发出4个存储器读取周期来获取整数。在一个内存周期中读取整数的所有4个字节更为经济。为了利用这种优势,如上图所示,存储器将按4个存储体的组进行排列。
内存寻址仍然是顺序的。如果存储区0占用地址X,则存储区1,存储区2和存储区3将位于(X + 1),(X + 2)和(X + 3)地址。如果在X地址上分配了4个字节的整数(X是4的倍数),则处理器仅需要一个存储周期即可读取整个整数。
反之,如果整数是在非4的倍数的地址处分配的,则它跨越存储体的两行,如下图所示。这样的整数需要两个内存读取周期才能读取数据。
变量的数据对齐方式处理存储在这些存储库中的数据的方式。例如, int在32位计算机上的自然对齐方式为4个字节。当数据类型自然对齐后,CPU会以最少的读取周期来获取数据。
同样, short int的自然对齐方式为2个字节。这意味着,可以将一个简短的int存储在bank 0 – bank 1对或bank 2 – bank 3对中。一个双精度型需要8个字节,并且在存储体中占据两行。 double的任何未对齐都会迫使两个以上的读取周期来获取double数据。
请注意,将在32位计算机上的8字节边界上分配一个double变量,并且需要两个内存读取周期。在64位计算机上,根据存储体数量,将在8字节边界上分配double变量,并且只需要一个内存读取周期。
结构填充:
在C / C++中,结构用作数据包。它不提供任何数据封装或数据隐藏功能(由于与类的语义相似性,C++情况是一个例外)。
由于各种数据类型的对齐要求,因此结构的每个成员都应自然对齐。结构的成员依次分配升序。让我们分析以上程序中声明的每个结构。
以上程序输出:
为方便起见,假定每个结构类型变量都在4字节边界上分配(例如0x0000),即结构的基址是4的倍数(不必总是必需,请参见structc_t的说明)。
结构A
structa_t的第一个元素是char ,它是一个字节对齐,后跟short int 。 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()返回的指针为void * 。根据程序员的需要,可以将其转换为任何数据类型。 malloc()的实现者应返回一个与原始数据类型(由编译器定义)的最大大小对齐的指针。通常在32位计算机上与8字节边界对齐。
目标文件对齐,节对齐,页面对齐
这些特定于操作系统实现者,编译器作者,不在本文的讨论范围之内。实际上,我没有太多信息。
一般的问题:
1.对齐是否应用于堆栈?
是的。堆栈也是内存。系统程序员应使用正确对齐的内存地址加载堆栈指针。通常,处理器不会检查堆栈对齐,程序员负责确保堆栈内存正确对齐。任何未对准都会导致运行时意外。
例如,如果处理器字长为32位,则堆栈指针也应对齐为4个字节的倍数。
2.如果将char数据放置在bank 0以外的bank中,则在读取内存时会将其放置在错误的数据线上。处理器如何处理char类型?
通常,处理器将基于指令识别数据类型(例如,ARM处理器上的LDRB)。根据存储的存储区,处理器将字节移到最低有效数据线上。
3.当参数在堆栈上传递时,它们是否要对齐?
是的。编译器帮助程序员进行正确的对齐。例如,如果将16位值压入32位宽的堆栈,则该值将自动用0填充到32位。考虑以下程序。
C
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个字节的边界上对齐double类型?
重要的是要注意,大多数处理器将具有称为浮点单元(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