なぜ (x << 2) & -4 のAND演算は消えるべきなのか
8ビットの数値 x = 10110111 を例に考えます。(出典)
xを2ビット左シフトすると11011100(下位2ビットは必ず0になる)- これを
-4(2の補数表現で下位2ビットだけをクリアするマスク)とAND演算しても、結果は変わらず11011100
つまり & -4 という演算は、シフト演算の性質上もともと何もしていないことになります。
最適化コンパイラであれば、
(x << 2) & -4を単なるx << 2にまで簡約できてしかるべきだ。
「範囲」だけでは「常に偶数」を表現できない
HotSpotのC2コンパイラは、ある変数が取りうる値の集合を「型」として内部管理しています。従来この型は [最小値, 最大値] という符号付き範囲だけで表現されていました。
| 手法 | x << 2 の情報 |
最適化への活用 |
|---|---|---|
範囲 [lo, hi] のみ |
ほぼint型全域([Integer.MIN_VALUE, 2147483644]) |
ほとんど活用できない |
| Known Bits併用 | 下位2ビットが確実に0 | & -4 のようなマスク演算を除去できる |
x が取りうる値が全く分からない場合、x << 2 の範囲はオーバーフローや符号反転を考慮するとint型のほぼ全域に広がってしまい、コンパイラにとってほとんど役に立ちません。
「known-0・known-1・不明」を追跡する仕組み
そこでC2は、範囲情報に加えて32ビットの数値それぞれについて「確実に0(zeros)」「確実に1(ones)」を示す2つのビットマスクを併せ持つようになりました。
template <class U>
class KnownBits {
U _zeros;
U _ones;
bool is_satisfied_by(U v) const {
return (v & _zeros) == U(0) && (v & _ones) == _ones;
}
};
x << 2 の場合、下位2ビットは zeros に立ち、それ以外は不明のままです。これにより、コンパイラは「この数値は4の倍数である」ことを把握でき、& -4 のようなマスク演算を安全に削除できるようになります。
この仕組みはHotSpot固有のものではなく、LLVMの KnownBits(同じzero/oneビットマスクと zeros & ones == 0 という不変条件を持つ)が最も近い実装だとされ、GCCも同様の情報を追跡しているといいます。
普段Python・TypeScriptを中心に書いている身としては、ここまで低レベルな最適化に触れる機会は多くありません。ただ、自分が日常的に使っているV8やCPythonのような処理系の内部でも、似たような「値についての知識」を積み上げる仕組みが動いているのだろうと想像すると、普段意識しない部分への解像度が少し上がる感覚があります。