位运算对齐公式推导
为什么 ((addr + align - 1) / align) * align 能等价地变成 (addr + align - 1) & !(align - 1)?
核心前提:align 必须是 2 的幂
只有当 align = 2^k(比如 2、4、8、16…)时,这个转换才成立。如果不是 2 的幂,就不能用位运算。
第一步:理解"除以 2^k"和"对 2^k 取余"在位运算里是什么
先看一个关键事实:
align - 1 的低 k 位全是 1。
例如 align = 8 = 2^3,那么 align - 1 = 7 = 0b111,低 3 位全是 1。
由此推出两个等价关系:
addr % align == addr & (align - 1) // 取余 = 保留低 k 位
addr / align * align == addr & !(align - 1) // 抹掉余数 = 低 k 位清零
为什么?因为 align 是 2 的幂,它的倍数在二进制里都是"低 k 位为 0"的数。
- 一个数除以
align的余数,恰好就是它的低 k 位。 - "把低 k 位清零",就是把余数抹掉,得到"不大于它、且是 align 倍数"的数,也就是向下取整到 align 倍数。
第二步:验证"& !(align-1) = 向下取整"
以 addr = 100, align = 8 为例:
100 = 0b0110_0100
7 = 0b0000_0111
!7 = 0b1111_1000
100 & !7 = 0b0110_0100 & 0b1111_1000
= 0b0110_0000
= 96
96 就是"100 向下取整到 8 的倍数"(96 ≤ 100,且 96 是 8 的倍数)。
所以:
addr & !(align - 1) == (addr / align) * align // 向下取整
第三步:把"向上取整"转成"向下取整"
我们要的是向上取整,不是向下。有一个经典技巧:
向上取整到 align 的倍数 = 向下取整到 (addr + align - 1) 的 align 倍数
也就是:
align_up(addr) = align_down(addr + align - 1)
为什么加 align - 1 就变成向上了?因为:
- 如果
addr已经是 align 的倍数,加align - 1后不会跨到下一个倍数(因为addr + align - 1 < addr + align,取整后还是addr)。 - 如果
addr有"一点点余数",加align - 1就一定能把它推进到下一个倍数。
第四步:拼起来
把第三步的结论代入第二步的位运算:
向上取整 = align_down(addr + align - 1)
= (addr + align - 1) & !(align - 1)
这就得到了最终公式。
完整推导链(一句话版)
((addr + align - 1) / align) * align // 算术:向上取整
↓ 因为 align = 2^k
(addr + align - 1) & !(align - 1) // 位运算:等价实现
核心就两条:
- 加
align - 1→ 把"向上取整"变成"向下取整"。 & !(align - 1)→ 把"除以 align 再乘 align(抹余数)"变成"低 k 位清零"。
一个更直观的类比
把地址想成"厘米",align=8 想成"只能以 8 厘米为单位的刻度尺"。
& !(align-1)= 把尺子往左推到最近的 8 的刻度(向下)。- 先
+ align - 1= 先往右挪一点(最多挪 7 厘米),再往左推,结果就等价于"往右推到最近的刻度"(向上)。
小结
& !(align - 1)单独看是向下取整(抹掉低 k 位 = 去掉余数)。- 前面
+ align - 1的作用是把"向下取整"转成"向上取整"。 - 两者结合,就等价于
((addr + align - 1) / align) * align。 - 前提永远是
align是 2 的幂。
你只要记住这条链:加(align-1) 变向上 → &!(align-1) 抹余数,就能自己推导出来,不用死记公式。