The mask that compiles to nothing: how HotSpot's JIT learned to reason about bits | QuestDB<br>New: QuestDB For AI Agents<br>New: QuestDB For AI Agents<br>Learn more<br>When a developer types (x , an optimizing compiler should compile it to just the shift,<br>x : the bitwise AND should disappear. Why? Imagine we have an 8-bit number x = 1011 0111 and my expression looks like (x
x 1011 0111 (the original x)<br>& -4 1111 1100 (-4 in two's complement -> the mask only clears lowest 2 bits)<br>result 1101 1100 (same as x
But how does the C2 compiler actually do this? The optimization relies on a general abstraction, built over multiple changesets.<br>This post is my attempt to understand it and explain it.
What does the JIT actually know about your numbers?
When C2 is compiling a method, what does it know about the value in a given variable?
The answer is "a set of possible values." The compiler usually can't know the exact runtime value of<br>x (that's the whole point of a variable), but it can often prove that x is<br>constrained. If it can prove the constraint is tight enough, it can rewrite the code. A classic<br>example: if the compiler proves an array index is always in [0, length), it deletes<br>the bounds check. Constant folding, dead branch elimination, and other optimizations often come down to<br>"prove the set of possible values is small enough to act on."
C2 stores this "set of possible values" as a type. Don't imagine a Java types int or long, but a much<br>richer internal type that carries a range. For most of HotSpot's life, an integer type was<br>essentially:
[lo, hi] // the value is somewhere in this signed range, inclusive
So the type of x & 0xFF would be [0, 255], and the compiler could use that. This signed range is simple and useful,<br>but it is too weak for our purpose.
Consider x . What's the range of x if x can be anything? Well... almost anything.<br>Shifting left can overflow, wrap around, produce huge positives and huge negatives. The tightest<br>signed interval covering the result is [Integer.MIN_VALUE, 2147483644]. It is a huge range, just shy<br>of the full int range, and it gives the compiler almost nothing useful.
But we know something very specific about x : No matter what x is the bottom two bits are<br>always zero! A range can't express that. [lo, hi] can say<br>"the value is small" but it can't say "the value is even," let alone "the value is a multiple of<br>four." That knowledge lives in the bits, and the range alone cannot express it.
Enter known bits
The fix: alongside the range, also track what we know about each individual bit. For a 32-bit integer, imagine two extra 32-bit masks travelling with the type:
zeros: a 1 in position i means "bit i is definitely 0"
ones: a 1 in position i means "bit i is definitely 1"
A bit that's unknown is 0 in both masks. A bit can't be both, so the invariant zeros & ones == 0<br>must always hold. In C2 it's a dozen lines of rangeinference.hpp:
template<br>class KnownBits {<br>static_assert(U(-1) > U(0), "bit info should be unsigned");
public:<br>U _zeros;<br>U _ones;
bool is_satisfied_by(U v) const {<br>return (v & _zeros) == U(0) && (v & _ones) == _ones;<br>};
is_satisfied_by just checks whether a given number v is allowed by the masks. The masks must never<br>rule out a value that can actually occur, and C2's tests and internal checks use this to verify they<br>don't.
Every bit is now in one of three states: known-0 , known-1 , or unknown . Here's the type of x , with . for unknown:
bit: 31 2 1 0<br>x: . . . . . . . . . . . . . . . . . . . (x is fully unknown)
So x has zeros = 0b11 and everything else unknown.<br>The compiler now knows the number is a multiple of four.
This data structure isn't unique to HotSpot. LLVM's<br>KnownBits<br>is the closest match to C2's representation (a Zero and a One bitmask with the same<br>Zero & One == 0 invariant), and GCC tracks the same thing as the mask in its<br>conditional constant-propagation pass<br>(a value plus an uncertainty mask). It's the same idea: a cheap, non-relational, per-bit<br>abstraction of an integer. HotSpot got the foundations<br>(JDK-8315066, by Quan Anh Mai and Emanuel Peter) in<br>JDK 26, and the pieces that actually delete (x landed in JDK 27. Fun fact: Quan Anh Mai is merykitty, whose magic SWAR line<br>parser we took apart during 1BRC.
INFO
Non-relational : the abstraction tracks each bit on its own and never records relationships<br>between bits (no "bit 3 is always equal to bit 5"). That independence is what makes it cheap: constant<br>per-bit bookkeeping.
Two views that refine each other
We now have two different descriptions of the same number side by side: a range and a set of<br>known bits . Each one knows things the other doesn't, and they can teach each other:
The bits know the low two bits are zero. The range uses that: if it currently says [5, 41], then<br>5, 6, 7, and 41 all have nonzero low bits and can't occur, so the range tightens to [8, 40], the<br>nearest multiples of four inside it.
The range knows the value is in [0, 200]. The bits use that: every number from 0 to...