内存对齐的学习笔记
作者:laomai
blog: http://blog.csdn.net/laomai
一、问题的提出
两年之前我写过一篇可变参数学习笔记,里面曾经简单的解释过一句代码:
((sizeof(n)+sizeof(int)-1)&~(sizeof(int)-1))
这个宏的作用是在考虑字节对齐的因素下计算变量n占用的字节大小,以便求出下一个可变参数的起始地址。
当时限于时间和水平,未能做更详细的解释。
今天(2007-11-26)在csdn论坛上看到了一个帖子
http://topic.csdn.net/u/20071123/16/c8d17d3f-9f49-49af-a6d8-1d7a7d84dc1c.html?seed=303711257
问题:CRT源码分析中一个关于可变函数参数的问题
提问者:Sun_Moon_Stars
这个帖子里面又问到了这个宏,于是决定抽出半天时间,把这个问题详细的说清楚。也算是把我的那篇文章做一个完美的结尾。
二、引子
先看一个日常生活中的问题,
问题1:假设有要把一批货物放到集装箱里,货物有12件, 一个箱子最多能装6件货物,求箱子的数目。
解答:显然我们需要12/6=2个箱子,并且每个箱子都是满的。这个连小学生都会算:-)
问题2: 把问题1的条件改一下,假设一个箱子最多能装5件货物,那么现在的箱子数是多少?
解答: 12/5=2.4个,但是根据实际情况,箱子的个数必须为整数,(有不知道这个常识的就不要再往下看了,回小学重读吧,呵呵)自然我们就要取3,
下面把上面的装箱问题一般化。
三、一般数学模型
问题3:设一个箱子最多可以装M件货物,现有N件货物,问需要多少个箱子,要给出一般的计算公式。
这里要注意两点:
1、箱子的总数必须为整数;
2、N不一定大于M。很显然,即使N <M,也得需要一只箱子
四、通项公式
1、预备知识
在讨论问题3的解答之前,我们先明确一下/运算符的含义。
定义/运算为求商运算,即
对任意两个整数N,M,必然有且只有唯一的整数X,满足
X*M <= N < (X+1)*M,那么记N/M=X。 (定义1)
这个也正是c里/运算符的确切含义。x的存在性和唯一性的严格证明可以见数论教材。
以后如无额外说明,/运算的含义均和本处一致。 另外,根据实际情况,
后面出现的所有大写字母如M、N、A、B、X、Y、Z等均表示正整数也就是自然数。
举几个/运算的例子:
(1) (M-1)/M=0, (等式1)
一般的,一个比M小的自然数对M取商的结果一定是0。
(2)设N满足M<=N<2*M,则N/M=1 (等式2)
(3)(M*X)/M=X
/运算有一个基本的性质。
若N=M*X+Y,则N/M=X+Y/M,证明略。
注意:这个公式的含义是/运算在一定条件下满足分配律。
即N不是可以随便拆的,设N=A+B,那么一般情况下(A+B)/M 不一定等于 A/M+B/M,
当A和B至少有一个是M的倍数时,才可以保证N/M=A/M+B/M。
2、分步讨论
根据上面的/运算符的定义,我们可以得到问题3的初步解答,分情况讨论一下
已知N/M=X,那么当
(1) 当N正好是M的倍数,即N=M*X时,那么所求的箱子数就是X=N/M
(2) 如果N不是M的倍数,即N=M*X+Y(显然1 <=Y <M)时
那么还要多一个箱子来装余下的Y件货物, 则箱子总数为X+1 = N/M+1
3、一般公式
上面的解答虽然完整,但是用起来并不方便,因为每次都要去判断N和M的倍数关系,
效率不高。于是有高手怒了,就想找一个统一的公式。因此,下面的终极公式出现了,
一个箱子最多可以装M件货物,且现有N件货物,那么所需的箱子数为
(N+M-1)/M (公式1)
这个式子用具体数字去验证是很简单的,留给读者去做。
我这里给一个完整的数学推导:
根据/运算的定义,设N/M=X,
则X*M <= N < (X+1)*M
分情况证明一下
(1)当N是M的倍数,即N=M*X时,
(N+M-1)/M
= (M*X)/M+(M-1)/M (分配律)
=X+0 (用到了等式1)
=X
(2)当N不是M的倍数时,设余数为Y。则有N=M*X+Y(1 <=Y <M),
将不等式1 <= Y < M的三项同时加上M-1,就得到
M <= Y-1+M <= 2*M-1 < 2*M
即 M <= Y-1+M < 2*M
于是根据等式2立刻得到
(Y-1+M) /M = 1
因此,
(N+M-1)/M
= (M*X+Y+M-1)/M
= (M*X)/M+(Y+M-1)/M (分配律)
= X+1
综合上面的(1)和(2),无论哪种情况,公式 (N+M-1)/M的计算结果都是正确的。
可能有的读者还会问,这个公式是怎么想出来的,怎么就想到了加上那个M-1?
这个问题可以去看看数论中的余数理论。
五、掩码的作用
在描述本文开头的宏的确切含义之前,还得先仔细的解释一下
~(sizeof(int)-1)) 的作用。
在以下的叙述中,均约定M是是2的幂,
即M=2z=power(2,Z), Z=log2M=log(2,M)。
1、首先要介绍两个位运算的技巧:若M=power(2,Z),则必有
N/M = N>>Z , (公式2)
N*M = N<<Z (公式3)
证明略。
2、结合上面的公式2和公式3
又可得到( N/M)*M =( N>>Z)<<Z (公式4)
我们来解释一下( N/M)*M的结果:
已知N/M=X,设Y=N-M*X,那么Y必然满足条件0<=Y<M,
这里的Y就是通常整数除法中的余数,而X=N/M就是整数除法的商。
当N,M,X,Y都用二进制表示时,那么N最右边的Z位数字就是余数Y.
剩下的左边数字就是商X。
于是,N>>Z相当于去掉余数Y,只留下商X,即X=N/M=N>>Z
然后(N>>Z) << Z=X<<Z=X*M,又将商扩大了M倍
最终得到的二进制数字形式为:
X00000..0, ——数字的左边是商X,右边是Z个0。
因此, ( N>>Z)<<Z的效果就是将N中的余数部分清零,商依然放在原处。
3、当M = power(2,Z) 时
(N >>Z) << Z = (N &(~(M-1)) (公式5)
也是一个恒等式。
证明如下:
(1) 因为M=power(2,Z),因此M的二进制形式为
1000..00, 右边共有Z个0,
但是在实际情况中,存储M时必须要占一个固定的位数S,左边未满的要补0,
于是当存储位数为S时,M= power(2,Z)的完整存储形式是
M=0000..0010000000,
M的右边还是Z个0,而左边添上了S-Z-1个0
(2)根据二进制的减法,有
M-1 = 00000....01111...1,
M-1的右边有Z个1,左边有S-Z个0
(3)根据求补运算~ 的定义,有
~(M-1)= 11111... 10000...000
~(M-1)的右边有Z个0,左边有S-Z个1
现在我们就能看出~(M-1)的效果了:它的右边Z+1位就是原来的M,而左边全是1。
(4)~(M-1)更专业的叫法就是掩码(mask)。任何一个二进制数字N和这个掩码进行&运算后,N的最右边Z位的数字全部被置0(被"掩抹"掉了).而左边保持不变。这和(N >>Z) << Z的计算结果一致。因此
(N >>Z) << Z = (N &(~(M-1) )
证毕。
六、内存对齐宏的分析
结合上面的叙述,内存对齐的问题可以归纳如下:
设一个机器字(int类型)的长度为M字节,根据实际情况,机器字的长度都是2的幂
(常见的有8位机、16位机、32位机、64位机等),
可以设M=sizeof(int)=power(2,Z),
已知某个类型的变量n的原始大小为N=sizeof(n),则在考虑内存对齐的情况下,
该变量所占用的总机器字的数目为
((N+M-1)/M) (根据公式1)
所以,
n占用的总字节数=机器字数目*一个机器字所占的字节数
= ((N+M-1)/M)*M
= ((N+M-1)>>Z)<<Z (根据公式4)
=(N+M-1) & ~(M-1) (根据公式5)
= ((sizeof(n)+sizeof(int)-1)&~(sizeof(int)-1))
最后一步就是我们看到的宏。
因此这个宏求的就是变量n在对齐情况下占用的总字节数目。
注意:
这里最关键的一点就是M必须是2的幂(有人常常理解成2的倍数也可以,那是不对的),
小结:
1、内存对齐的原理类似与装箱问题,其核心就是数论中的求商运算和乘法运算。首先用/运算求出占用的机器字数目(相当于箱子个数),再乘上每个机器字的长度即得到最后的总长度(相当于箱子总容量)。
2、由于机器字的长度M都是2的幂,所以对M的/运算和乘法运算可以用与运算和掩码来实现以提高效率。