C语言再学习之内存对齐
昨天看Q3的代码,看到有个_INTSAIZEOF的宏,着实晕了一阵。一番google后,终于明白,这个宏的作用是求出变量占用内存空间的大小,先看看_INTSAIZEOF的定义吧:
#define _INTSIZEOF(n) ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )
(ANSI C标准下,_INTSAIZEOF宏定义在stdarg.h中,Q3中定义在bg_lib.h中;bg_lib.h -- standard C library replacement routines used by code)
关于这个宏的内部原理,我们后面再谈,还是先理理“内存对齐”这词的意思吧,多年来一直模糊的存在于我的大脑中,究竟为什么要内存对齐啊。
内存对齐的根源:
1、许多计算机系统对基本数据类型可允许地址作了一定的限制,要求某种类型对象的地址必须是某个值n(通常是2、4、8)的倍数,从而来简化处理器和存储器之间的接口的硬件设计。如Linux的对齐策略是2字节数据类型,例如short的地址必须是2的倍数。而较大的数据类型如:int、int*、float、double则必须是4的倍数。而Microsoft Windows的策略要求更为严格-----任何k字节对象的地址必须是k的倍数。比如要求一个double类型对象的地址必须是8的倍数(引自《深入理解计算机系统》)。
2、数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
内存对齐的规则:
上面所提到的自然边界是由什么决定的呢,我想应该是由硬件平台决定的,至于操作系统和编译器(默认对齐系数)则都是依赖于上一层的。当然,编译器的对齐系数可以通过预编译命令#pragma pack(n),n=1,2,4,8,16来改变;
举个例子:比如参数入栈,编译器并不是一个紧挨着一个的压入栈的,而是根据对齐系数来压栈的,比如一个char类型的参数,虽然本身只占一个字节,但是编译器会自动补全后面3个字节,然后再压下一个参数。
(这里说点题外话:在写过delphi程序的人都知道,有个packed的保留字,作用就是压缩数据结构,不要按对齐存储,除非数据结构体积庞大,否则为什么要用packed呢,用了不就影响内存读取的速度了吗?:))
1、数据成员对齐规则:结构(struct)(或联合(union))的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员的对齐按照#pragma pack指定的数值和这个数据成员自身长度中,比较小的那个进行。
2、结构(或联合)的整体对齐规则:在数据成员完成各自对齐之后,结构(或联合)本身也要进行对齐,对齐将按照#pragma pack指定的数值和结构(或联合)最大数据成员长度中,比较小的那个进行。
3、结合1、2颗推断:当#pragma pack的n值等于或超过所有数据成员长度的时候,这个n值的大小将不产生任何效果。
(以上3点引自《也谈内存对齐》一文)
再看_INTSAIZEOF宏:
这个宏的定义咋一看有点丈二和尚摸不着头脑,不过网上有对齐的解释,看后相信会豁然明朗了:
对于两个正整数 x, n 总存在整数 q, r 使得
x = nq + r, 其中 0<= r <n //最小非负剩余
q, r 是唯一确定的。q = [x/n], r = x - n[x/n]. 这个是带余除法的一个简单形式。在 c 语言中, q, r 容易计算出来: q = x/n, r = x % n.
所谓把 x 按 n 对齐指的是:若 r=0, 取 qn, 若 r>0, 取 (q+1)n. 这也相当于把 x 表示为:
x = nq + r', 其中 -n < r' <=0 //最大非正剩余
nq 是我们所求。关键是如何用 c 语言计算它。由于我们能处理标准的带余除法,所以可以把这个式子转换成一个标准的带余除法,然后加以处理:
x+n = qn + (n+r'),其中 0<n+r'<=n //最大正剩余
x+n-1 = qn + (n+r'-1), 其中 0<= n+r'-1 <n //最小非负剩余
所以 qn = [(x+n-1)/n]n. 用 c 语言计算就是:
((x+n-1)/n)*n
若 n 是 2 的方幂, 比如 2^m,则除为右移 m 位,乘为左移 m 位。所以把 x+n-1 的最低 m 个二进制位清 0就可以了。得到:
(x+n-1) & (~(n-1))