我们所说的数据对齐、结构打包和填充是什么意思? 预测以下程序的输出。
C
#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" , sizeof (structa_t)); printf ( "sizeof(structb_t) = %lu" , sizeof (structb_t)); printf ( "sizeof(structc_t) = %lu" , sizeof (structc_t)); printf ( "sizeof(structd_t) = %lu" , sizeof (structd_t)); return 0; } |
在继续之前,把你的答案写在纸上,然后继续读下去。如果你迫切想看到解释,你可能无法理解你的类比中的任何漏洞。 数据对齐: C/C++中的每种数据类型都有对齐要求(实际上,它是由处理器体系结构而不是语言强制要求的)。处理器的处理字长与数据总线大小相同。在32位机器上,处理字大小为4字节。
历史上,内存是字节可寻址的,并按顺序排列。如果内存被安排为一个字节宽的单组,处理器需要发出4个内存读取周期来获取一个整数。在一个内存周期内读取整数的所有4个字节更经济。为了利用这种优势,存储器将被安排为4个存储组,如上图所示。 内存寻址仍然是顺序的。如果银行0占用地址X,银行1、银行2和银行3将位于(X+1)、(X+2)和(X+3)地址。如果在X地址上分配了一个4字节的整数(X是4的倍数),处理器只需要一个内存周期就可以读取整个整数。 其中,如果整数被分配到4的倍数以外的地址,则它跨越两行存储组,如下图所示。这样的整数需要两个内存读取周期来获取数据。
变量的 数据对齐 处理数据存储在这些银行中的方式。例如,自然排列的 智力 在32位机器上是4字节。当数据类型自然对齐时,CPU以最小的读取周期获取数据。 同样,自然排列的 短整型 是2字节。这意味着 短整型 可以存储在气缸组0–气缸组1对或气缸组2–气缸组3对中。A. 双重的 需要8字节,并占用内存库中的两行。任何不一致的 双重的 将强制两个以上的读取周期来获取 双重的 数据 请注意 双重的 变量将在32位机器上的8字节边界上分配,需要两个内存读取周期。在64位机器上,根据银行数量, 双重的 变量将在8字节边界上分配,只需要一个内存读取周期。 结构填充: 在C/C++中,结构用作数据包。它不提供任何数据封装或数据隐藏功能(C++case是一个例外,因为它与类的语义相似)。 由于各种数据类型的对齐要求,结构的每个成员都应该自然对齐。结构的成员按顺序递增分配。让我们分析上面程序中声明的每个结构。 上述程序的输出: 为了方便起见,假设每个结构类型变量都分配在4字节边界(比如0x0000)上,即结构的基址是4的倍数(不必总是必需的,请参见structc_t的解释)。 结构A 这个 结构 第一个要素是 烧焦 它是一个字节对齐的,后跟 短整型 .short int是2字节对齐的。如果短int元素紧跟在char元素之后,它将从奇数地址边界开始。编译器将在字符后插入一个填充字节,以确保短int的地址倍数为2(即2字节对齐)。结构的总大小为sizeof(char)+1(padding)+sizeof(short),1+1+2=4字节。 结构B 第一个成员 结构 是短int,后跟char。由于char可以位于任何字节边界上,短int和char之间不需要填充,因此它们总共占用3个字节。下一个成员是int。如果立即分配int,它将从奇数字节边界开始。我们需要在char成员之后填充1字节,以使下一个int成员的地址对齐4字节。总的来说 结构 需要2+1+1(填充)+4=8字节。 结构C——每个结构也有对齐要求 应用同样的分析, 结构 需要sizeof(char)+7字节填充+sizeof(double)+sizeof(int)=1+7+8+4=20字节。但是,sizeof(structc_t)将是24字节。这是因为,与结构成员一样,结构类型变量也会自然对齐。让我们通过一个例子来理解它。比如说,我们声明了一个结构数组,如下所示
structc_t structc_array[3];
假设 struct_数组 是0x0000,便于计算。如果我们计算的structc_t占用20(0x14)字节,那么第二个structc_t数组元素(索引为1)将位于0x0000+0x0014=0x0014。它是数组的索引1元素的起始地址。此结构的双成员将在0x0014+0x1+0x7=0x001C(十进制28)上分配,这不是8的倍数,与双成员的对齐要求相冲突。正如我们在顶部提到的,double的对齐要求是8字节。 为了避免这种不一致,编译器将向每个结构引入对齐要求。它将成为结构中最大的构件。在我们的例子中,structa_t的对齐是2,structb_t是4,structc_t是8。如果我们需要嵌套结构,最大内部结构的大小将是直接较大结构的对齐。 在上述程序的struct_t中,int member后面将有4个字节的填充,以使结构大小为其对齐的倍数。因此,结构的大小是24字节。即使在阵列中,它也能保证正确对齐。你可以交叉核对。 结构D–如何减少填充? 到目前为止,很明显,填充是不可避免的。有一种方法可以最小化填充。程序员应该按照大小的增减顺序声明结构成员。我们的代码中给出了一个示例structd_t,它的大小是16字节,而不是24字节的structc_t。 什么是结构化包装? 有时,在结构的成员之间必须避免填充字节。例如,读取ELF文件头或BMP或JPEG文件头的内容。我们需要定义一个类似于标题布局的结构,并对其进行映射。然而,在接触这些成员时应谨慎。通常,逐字节读取是避免未对齐异常的一种选择。将有对性能的影响。 大多数编译器都提供非标准扩展来关闭默认填充,比如pragmas或命令行开关。有关更多详细信息,请参阅相应编译器的文档。 指针事故: 在处理指针算法时,可能会出现潜在错误。例如,如下图所示取消对通用指针(void*)的引用可能会导致未对齐异常,
// Dereferencing 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.如果 烧焦 如果数据被放置在另一个存储组0中,则在内存读取期间,它将被放置在错误的数据行上。处理器如何处理 烧焦 类型 通常,处理器会根据指令识别数据类型(例如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" , ( 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) = 4sizeof(structb_t) = 8sizeof(structc_t) = 24sizeof(structd_t) = 16
更新:2013年5月1日 据观察,在最新的处理器上,我们得到的struct_c大小为16字节。我还没有阅读相关文件。一旦我得到了正确的信息(写给少数硬件专家),我就会更新。 在使用相同工具集(GCC 4.7)的旧处理器(AMD Athlon X2)上,我得到的结构大小为24字节。大小取决于内存库在硬件级别的组织方式。 ––由 文基 。如果您发现任何不正确的地方,或者您想分享有关上述主题的更多信息,请发表评论。