原文地址:浅谈VC中的字节对齐
前几天时,在公司和同事说到了字节对齐,一直对这个概念比较模糊,只是在《程序员面试宝典》中看到过简单的描述和一些面试题。后来在论坛中有看到有朋友在询问字节对齐的相关问题,自己也答不上来,觉得应该研究一下,所以就有了这一篇博文,是对学习的一个总结,也是对成长轨迹的一个记录。
字节对齐,又叫内存对齐,个人理解就是一种C++中的类型在内存中空间分配策略。每一种类型存储的起始地址,都要求是一个对齐模数(alignment modulus)的整数倍。问题来了,为什么要有这种策略?计算中内存中的数据就是一个一个的字节(byte),直接按照一个字节一个字节存储就得了,为什么还要那么麻烦。把问题想简单了。
各个硬件平台对存储空间的处理上有很大的不同。一些平台对某些特定类型的数据只能从某些特定地址开始存取。比如有些架构的CPU在访问 一个没有进行对齐的变量的时候会发生错误,那么在这种架构下编程必须保证字节对齐.其他平台可能没有这种情况,但是最常见的是如果不按照适合其平台要求对 数据存放进行对齐,会在存取效率上带来损失。
计算机CPU一次处理可以处理多个字节,就拿32位系统来说,CPU一次可以处理32bit的数据,也就是4个字节。比如有些平台每次读都是从偶地址开始,假设有一个int型数据,存放在内存地址0x1的位置。CPU要读取这个int数据,并且从地址0x0开始读取数据。一次读取4字节,那么这个int型还有一个字节没有读到,就得再读取一次剩下的那一个字节,并且还要进行位操作,把两次读取的数据合并为一个int型数据。两个字--麻烦,效率太低了。那怎么办呢?为了提高效率,干脆在存储的时候把这个int数据放在内存地址0x4的位置,0x1、0x2、0x3的位置都空着,CPU直接从0x4取数据,只需一次就取到了这个数据,还不用进行位操作。就是拿空间换时间,没办法,谁让现在的存内存越来越大了呢?
下面一些知识的总结,部分来自互联网,感谢那些为C++奋斗的兄弟。
字节对齐的规则
许多实际的计算机系统对基本类型数据在内存中存放的位置有限制,它们会要求这些数据的首地址的值是某个数k(通常它为4的倍数,这就是所谓的字节对齐,而这个k则被称为该数据类型的对齐模数(alignment modulus)。当一种类型S的对齐模数与另一种类型T的对齐模数的比值是大于1的整数,我们就称类型S的对齐要求比T强(严格),而称T比S弱(宽松)。下面来讨论4种不同类型的对齐模数:
- 内置类型的自身对齐模数(有符号无符号相同)
char 1
short 2
int 4
float 4
double 8
- 自定义类型的自身对齐模数(struct 、class)
等同于其成员中最大的自身对齐模数
- 指定对齐模数
我们给编译器指定的对齐模数(在VC中使用指令:#pragma pack(n),如果不指定,在VS2010默认为8)
- 有效对齐模数
指定对齐模数与类型自身对齐模数的较小的值,就是实际生效的对齐模数。
- 自定义类型中各个成员按照它们被声明的顺序在内存中顺序存储,第一个成员的地址和整个类型的地址相同
- 每个成员分别对齐,即每个成员按自己的方式对齐,并最小化长度,规则就是每个成员按其类型的对齐模数(通常是这个类型的大小)和指定对齐参模数中较小的一个对齐
- 结构、联合或者类的数据成员,第一个放在偏移为0的地方,以后每个数据成员的对齐,按照指定对齐模数和这个数据成员自身长度两个中比较小的那个进行。也就是说,当#pragma pack指定的值等于或者超过所有数据成员长度的时候,这个指定值的大小将不产生任何效果
- 自定义类型(如结构)整体的对齐(注意是“整体”)是按照结构体中长度最大的数据成员和指定对齐模数之间较小的那个值进行,这样在成员是复杂类型时,可以最小化长度
- 结构整体长度的计算必须是成员的所有对齐模数数中最大的那个值的整数倍,不够补空字节,因为对齐参数都是2的n次方。这样在处理数组时可以保证每一项都边界对齐
例如:
- struct alignment
- {
- char ch; // 自身对齐模数1,指定对齐模数8,有效对齐模数1
- int i; // 自身对齐模数4,指定对齐模数8,有效对齐模数4
- short sht; // 自身对齐模数2,指定对齐模数8,有效对齐模数2
- }; // 自身对齐模数4,指定对齐模数8,有效对齐模数4
在上例中,假设起始地址0x0,那么ch的地址为离0x0最近的且能被ch的有效对齐模数整除的地址,那么就是0x0;以此类推,i的地址为0x4,sht的地址为0x8,alignment的地址与ch的地址一致。
VC中的字节对齐设置
- 在VC IDE中,可以这样修改:[Project]|[Settings],c/c++选项卡Category的Code Generation选项的Struct Member Alignment中修改,默认是8字节。
- 在编码时,可以使用指令动态修改:#pragma pack
指令#pragma pack
作用:指定结构体、联合以及类成员的packing alignment;
语法:#pragma pack( [show] | [push | pop] [, identifier], n )
说明:
- pack提供数据声明级别的控制,对定义不起作用;
- 调用pack时不指定参数,n将被设成默认值;
- 一旦改变数据类型的alignment,直接效果就是占用memory的减少,但是performance会下降;
语法具体分析:
- show:可选参数;显示当前packing aligment的字节数,以warning message的形式被显示;
- push:可选参数;将当前指定的packing alignment数值进行压栈操作,这里的栈是the internal compiler stack,同时设置当前的packing alignment为n;如果n没有指定,则将当前的packing alignment数值压栈;
- pop:可选参数;从internal compiler stack中删除最顶端的record;如果没有指定n,则当前栈顶record即为新的packing alignment数值;如果指定了n,则n将成为新的packing aligment数值;如果指定了identifier,则internal compiler stack中的record都将被pop直到identifier被找到,然后pop出identitier,同时设置packing alignment数值为当前栈顶的record;如果指定的identifier并不存在于internal compiler stack,则pop操作被忽略;
- identifier:可选参数;当同push一起使用时,赋予当前被压入栈中的record一个名称;当同pop一起使用时,从internal compiler stack中pop出所有的record直到identifier被pop出,如果identifier没有被找到,则忽略pop操作;
- n:可选参数;指定packing的数值,以字节为单位;缺省数值是8,合法的数值分别是1、2、4、8、16。
使用示例:
- #include<iostream>
- using namespace std;
- #pragma pack(show) // Output中输出如下信息:warning C4810: value of pragma pack(show) == 8
- #pragma pack(push, alignmentDEfault) // 使用标识符alignmentDEfault压栈默认字节对齐模数
- #pragma pack(1) // 将对齐模数设置为1
- #pragma pack(show)
- #pragma pack(push, alignment1) // 使用标识符alignment1压栈默认字节对齐模数
- struct alignment1
- {
- char ch;
- int i;
- short sht;
- };
- #pragma pack(2) // 将对齐模数设置为2
- #pragma pack(show)
- #pragma pack(push, alignment2) // 使用标识符alignment2压栈默认字节对齐模数
- struct alignment2
- {
- char ch;
- int i;
- short sht;
- };
- #pragma pack(push, alignment8, 8) // 使用标识符alignment2压栈默认字节对齐模数
- #pragma pack(show)
- struct alignment8
- {
- char ch;
- int i;
- short sht;
- };
- #pragma pack(pop, alignmentDEfault) // 将标号alignmentDEfault对应的字节对齐模数弹出栈
- #pragma pack(show)
- struct alignmentDefault
- {
- char ch;
- int i;
- short sht;
- };
- int main()
- {
- alignment1 align1;
- cout << (int)&align1.i - (int)&align1.ch << endl; // 输出1
- cout << (int)&align1.sht - (int)&align1.i << endl; // 输出4
- alignment2 align2;
- cout << (int)&align2.i - (int)&align2.ch << endl; // 输出2
- cout << (int)&align2.sht - (int)&align2.i << endl; // 输出4
- alignment8 align8;
- cout << (int)&align8.i - (int)&align8.ch << endl; // 输出4
- cout << (int)&align8.sht - (int)&align8.i << endl; // 输出4
- alignmentDefault alignmentD;
- cout << (int)&alignmentD.i - (int)&alignmentD.ch << endl; // 输出4
- cout << (int)&alignmentD.sht - (int)&alignmentD.i << endl; // 输出4
- }
程序中的字节对齐与空间占用
字节对齐规则影响着struct和class的内存占用。来看一个例子:
- #include <iostream>
- #pragma pack(8)
- struct example1
- {
- short a;
- long b;
- };
- struct example2
- {
- char c;
- example1 struct1;
- short e;
- };
- #pragma pack()
- int main(int argc, char* argv[])
- {
- example2 struct2;
- cout << sizeof(example1) << endl;
- cout << sizeof(example2) << endl;
- cout << (unsigned int)(&struct2.struct1) - (unsigned int)(&struct2) << endl;
- return 0;
- }
程序中第2行#pragma pack (8)虽然指定了对齐模数为8,但是由于struct example1中的成员最大size为4(long变量size为4),故struct example1仍然按4字节对齐,struct example1的size为8,即第22行的输出结果;
struct example2中包含了struct example1,其本身包含的简单数据成员的最大size为2(short变量e),但是因为其包含了struct example1,而struct example1中的最大成员size为4,struct example2也应以4对齐,#pragma pack (8)中指定的对齐对struct example2也不起作用,故23行的输出结果为16;
由于struct example2中的成员以4为单位对界,故其char变量c后应补充3个空,其后才是成员struct1的内存空间,24行的输出结果为4。
字节对齐与程序的编写
如果在编程的时候要考虑节约空间的话,那么我们只需要假定结构的首地址是0,然后各个变量按照上面的原则进行排列即可,基本的原则就是把结构中的变量按照类型大小从小到大声明,尽量减少中间的填补空间.还有一种就是为了以空间换取时间的效率,我们显示的进行填补空间进行对齐,比如:有一种使用空间换时间做法是显式的插入reserved成员:
struct A{
char a;
char reserved[3];//使用空间换时间
int b;
}
reserved成员对我们的程序没有什么意义,它只是起到填补空间以达到字节对齐的目的,当然即使不加这个成员通常编译器也会给我们自动填补对齐,我们自己加上它只是起到显式的提醒作用.
代码中关于对齐的隐患,很多是隐式的。比如在强制类型转换的时候。例如:
unsigned int i = 0x12345678;
unsigned char *p=NULL;
unsigned short *p1=NULL;
p=&i;
*p=0x00;
p1=(unsigned short *)(p+1);
*p1=0x0000;
最后两句代码,从奇数边界去访问unsignedshort型变量,显然不符合对齐的规定。
在x86上,类似的操作只会影响效率,但是在MIPS或者sparc上,可能就是一个error,因为它们要求必须字节对齐.
如果出现对齐或者赋值问题首先查看
- 编译器的big little端设置
- 看这种体系本身是否支持非对齐访问
- 如果支持看设置了对齐与否,如果没有则看访问时需要加某些特殊的修饰来标志其特殊访问操作。