为啥要内存对齐
首先我们需要知道内存对齐的粒度,也就是操作一个内存单元的粒度。计算机系统中对齐的粒度一定都是2的n次方(计算机组成原理决定的,不懂的去学一下,这里不深入了),这个数用二进制表示一定是1000…000(是2的几次方,就是1后面跟几个0)
注意:我们这里的内存单元不一定是物理意义上内存单元,取决于你的代码。如果你的代码申请和读取内存都是按8Bytes为单位进行的,那你的内存对齐可能就需要用8Bytes来处理。
内存对齐的意思,粗略的讲就是处理成内存单元的整数倍,例如大小为6字节的数据,要给他分配8字节的空间,一个14字节的数据,要给他分配2个8字节。这样就避免了一些性能浪费,6字节的数据本来可以一次读取,如果不进行内存对齐,可能会造成横跨了两个内存单元,要读取两次内存。
举个例子
|—6Bytes—|—6Bytes—|
|----8Bytes----|----8Bytes----|
在读取第二个6Bytes的时候,要读取两个8Bytes的内存单位,才能将第二个6Bytes的数据完全读起来。因为他在两个内存单元里都留了一部分数据。
|—6Bytes-----|—6Bytes-----|
|----8Bytes----|----8Bytes----|
如果做了对齐,也就是每个6Bytes放在一个8Bytes的内存单位内,读取第二个6Bytes,只需要一次读取,将第二个8Bytes读取就可以。
内存对齐的方法
步骤一:按内存单元取整数倍
刚才提到的问题,解决办法是内存对齐要做的第一件事:按内存单元的粒度取整数倍,这样才能保证让数据不要横跨内存单元。
当然有些情况是不可避免的,比如14Bytes,是必须要跨两个内存单元。
用位运算去做这个事情就是 size & ~(ALIGNMENT - 1),下面我们详细解释一下
内存单元的大小用二进制表示一定是100…000(刚才说过了),减1以后就是00…001111(后面的0全变成1),这个我们称为对齐掩码 mask
再对mask取个反,就变成前面全是1,后面跟着n个0,11111…110000,
最后用size & ~mask,得到的就是size按内存单元取整的结果,因为最后面的n个位被&运算处理成了0。
步骤二:向上取整
经过上面的步骤完成了按倍数计算,但是出现一个新问题,如果要分配的size还没有一个内存单元大,那么按倍数取整以后就会是0,这个显然是不对的,我们总不能给要写入的数据分配0个字节。
那么怎么解决这个问题呢?我们想给要写入的数据加上一个内存单元不就好了嘛,6Bytes的数据,我们加上一个8Bytes,这样再去求整数倍,肯定能保证分配一个内存单元。(这个我们称为向上取整,就是为了保障最小的分配空间)
不错,这样处理是对的,但是有个corner case,如果要申请数据空间就是8Bytes,再加8Bytes,这样就会申请16Bytes,也就是2个内存单位,其实刚好1个内存单元就可以刚好存下。咋办呢?
答案是, 用size + (mask)去申请,mask是谁?内存单元大小 - 1。size + 内存单元大小 - 1 = size - 1 + 内存单元大小,如果size - 1不够一个内存单元,就只需要分配一个内存单元。如果size - 1等于内存单元大小,意味着不得不分配两个内存单元。
举个栗子
假设对齐的粒度是8字节,我们记为 MEM_ALIGNMENT ,这个常量是000…1000(最后三个0)
MEM_ALIGNMENT-1,就是00…000111(最后三个1)
再进行位反运算,那就是11…111000(最后三个0),一个数&上这个数,最后3位一定为0,意思也就是按8字节取整
((size) + MEM_ALIGNMENT - 1) & ~(MEM_ALIGNMENT-1))
我们记 (size) & ~(MEM_ALIGNMENT-1) 为向下取整
((size) + MEM_ALIGNMENT - 1) & ~(MEM_ALIGNMENT-1)) 为向上取整
size=0,则向下取整(size)=0, 向上取整(size)=0.
size=6, 则向下取整(size)=0 (错误), 向上取整(size)=8.
size=8, 则向下取整(size)=8, 向上取整(size)=8. (边界情况)
size=14, 则向下取整(size)=8 (错误), 向上取整(size)=16.