结构体struct的自然对齐问题
下面的机构体
struct A
{
char c;
int i;
char cc;
};
它的sizeof()是多少呢? 如果是紧凑对齐的话,当然是sizeof(char)+sizeof(int)+sizeof(char); 但编译器默认编译的却不是紧凑对齐的,在32-bit的机器上编译结果是12, 这是一个char占用了一个int的空间,换句话说,在c和i之间有三个空余的byte! 这就是自然对齐的效果。
为什么要自然对齐呢?
当初百度的gg电面我的时候,我胡乱的解释了一下,居然是对的,真是佩服我的扯淡能力。原来x86架构下的cpu访存时,是需要地址对齐的,举个例子,取short型时,地址需要是2的倍数,取int型是地址需要是4的倍数,如果不是的话,则可能降低cpu访存的效率。例,一个int的地址是0x03的话,先是按地址0x00取int的第一个byte,然后按地址0x04取剩下的三个byte,所以原本一次访存搞定的事情,却做了两次,简直是脱裤子放屁--多此一举嘛,哈哈。
怎么计算自然对齐的sizeof()呢?
这个才是面试官的杀手锏,哈哈。
其实就是一个规律--这个取决于cpu的字长。
在32bit的cpu上,对齐的单位是4个byte,64bit的我没有试过,谁有环境搞一下哈。
例:
struct B
{
char ca;
char cb;
char cc[10];//这段char+char数组是连在一起的,按char对齐
int i;
char cd[3];//有一个byte是空着的哦
};
sizeof()是20; 注意,连续的char总是按char对齐,多出部分的char才是按int对齐的,细心的你,也许发现linux上的网络头部的头文件中,比如ip的tcp的,都是用char或char数组定义的,这样就避免了自然对齐的问题,是的总是按char的紧凑对齐,增强程序的可移植性。我曾经就犯过这样的错误,解析网络头部的时候,struct里面同时出现int和char,自然对齐的结果是总是无法正确的解析出arp协议,郁闷我半年才发现这个bug。其实,这不是bug,这是语言的特性(引自《C专家》)。
网络上也有别的朋友说,是按照结构体里面最大的单元对齐的,这种说法是错误的,原因在于,他举的case全是int最大,而32bit的cpu总是按32-bit对齐正好是int的长度,这样就产生了错误的认识。
例
struct C
{
double d;
char c;
};
sizeof()的大小是12,不是2*sizeof(double)哦。
struct D
{
struct B b;
struct C c;
};
sizeof()的大小就是sizeof(B)+sizeof(C)的大小啦,可不是2*sizeof(B)哦;
怎么让结构体紧凑对齐呢?
gcc提供一个选项-fpack-struct,可是实现紧凑对齐的编译,其他平台可以借助#pragma pack来搞定,具体看网页http://www.mscenter.edu.cn/blog/mingge/archive/2005/12/10/7655.html
----------------------------------------------------------------------------------------------------------------------
结构体以其内部最大的基本数据类型和编译器字节对齐设置中的较小值为单位对齐。
比如:struct A{ int i; char c;};就以int型(4字节)对齐,所以整个结构体大小为8.
而:struct B{char k;char c;};以char型(1字节)对齐,所以整个结构体大小为2。
基本数据类型以该类型长度和编译器字节对齐设置中的较小值对齐。
VC中#pragma pack(1)设置字节以1字节对齐。默认是以的字节对齐。且只能设置为2的幂。
例如:struct C{char k;int i;};的大小为8,且i的地址是从k的后面三个字节开始的,因为int以4字节对齐。
再如:#pragma pack(1) struct D{char k;int i;}; #pragma pack() //恢复;的大小为5,因为int是以1字节对齐的。且整个结构体也受int型的对齐大小影响而变成1字节对齐。
----------------------------------------------------------------
MIPS体系结构
最近问我MIPS体系结构相关问题的人越来越多,在这里小结一下。
一般在职业有段者水平的MIPS体系结构问题,都能在这里找到答案。
1.
Q: MIPS有多少一般用途的寄存器?
A: 32个。
2.
Q: 我在看反汇编代码的时候,看到一些寄存器名叫zero, a1, a2...还有sp, ra这样的名字。为什么给他们起这些名字呢?
A: MIPS的通用寄存器中,按照编译器的通常的约定,某些寄存器是做专门用途的,比如sp就是堆栈指针,ra是函数调用的返回地址等等。当然你也可以不按照这些约定编程,例如用$2存放堆栈指针,$3存放返回地址,但这样的程序,和标准库链接就不能工作了。
3.
Q: 为什么MIPS的SP寄存器也是通用寄存器呢?而且MIPS似乎没有专门的压栈/出栈指令啊。
A: 这是MIPS这样的RISC处理器,同x86为代表的CISC处理器的重大区别之一。RISC没有专门的硬件实现的堆栈寄存器/指令,改为软件实现,在函数入口堆栈指针递减,堆栈向低地址生长,返回处恢复堆栈值,销毁堆栈帧。
4.
Q: MIPS的堆栈用软件实现,那么,MIPS的函数调用开销会更大吗?
A: 这是x86为首的CISC支持者,经常诟病RISC的一点。但是,实际上,我们知道,MIPS使用了4个GPR(一般用途寄存器)来传递前4个Word的函数参数,而x86只有EAX一个寄存器用于传递函数参数。所以,如果函数的参数小于或等于4个word,那就不需要使用栈。我们知道,大多数函数的参数都在4 个word以内。——所以,这一点不比太担心。另外,写太多参数的函数时,建议使用结构体传递这些参数。
5.
Q: MIPS的lh, lb这样的指令,会改变寄存器里的前2个/1个字节,还是后2个/1个字节?
A:一般来说是低位。
6.
Q: MIPS是大端(Big-endian)的,还是小端的(Little-endian)?
A: 这是可以配置的。一般来说我们都使用大端模式,因为几乎所有的网络协议,数据包中的数据字段均以大端模式封装。
7.
Q: 我在新闻报道看到“龙芯”和MIPS的指令95%相同,那么到底是哪部分不相同呢?
A: MIPS的四条“非对齐加载/存储”指令,是受专利法保护的,龙芯在没有获得授权的情况下不能支持这四条指令。
8.
Q: 什么是非对齐加载/存储指令?什么情况下用这些指令呢?
A: MIPS的内存访问要求字节对齐。譬如,4字节访问指令,要求访问的开始地址能被4整除,如0x8100223C. 如果访问0x8100223D这个地址,就会发生一个异常(Exception)。MIPS处理器在处理这个异常时,把这样一次加载拆分为两次非对齐的加载来实现。
9.
Q: 可是x86上,似乎没有这样的限制啊?
A: 其实x86也有这个问题,非对齐访问会被拆成两次执行。只不过这一切都由硬件来完成,对代码级别透明——这样,一个不理解这个原理的程序员就非常容易在设计数据结构的时候犯下这方面的错误,大量非对齐访问严重地影响系统的性能。
------------------------------------------------------------------------------------------------------------------------------
Mips CPU 要求内存地址必须对齐
1. Mips CPU 只能通过Load/Store两条指令访问内存
RISC的指令一般比较整齐,单条指令的功能单一,执行时间比较快。只能对寄存器中的数据运算,存储器的寻址一般只能通过L/S(Load/Store)进行。一般为等长指令,更便于流水线。
MIPS为RISC系统,等长指令,每条指令都有相同的长度:32位。其操作码固定为:6位。其余26位为若干个操作数。
2. 内存地址的对齐
对于一个32位的系统来说,CPU 一次只能从内存读32位长度的数据。如果CPU要读取一个int类型的变量并且该变量的起始位不在所读32位数据的首位,那么CPU肯定无法一次性读完这个变量,这时就说这个变量的地址是不对齐的。相反,如果CPU可以一次性读完一个变量,则说该变量的地址是对齐的。
3. Mips CPU 要求内存地址(即Load/Store的操作地址)必须是对齐的
其实不管是Mips,还是X86,都希望所操作地址是对齐的,因为这样可以最快速地处理数据。
不过X86平台可以很容易很快速地处理不对齐的情况,而Mips一旦遇到地址不对齐的变量就会抛出exception,从而调用一大段后续处理代码,继而消耗大量的时间。
因此,不管工作在什么平台下,程序员都应该养成使内存地址对齐的好习惯。