The mask that compiles to nothing: how HotSpots JIT learned to reason about bits
The Mask That Compiles to Nothing: How HotSpot's JIT Learned to Reason About Bits
When a programmer writes the expression (x << 2) & -4, an intelligent optimizing compiler should recognize that the bitwise AND is redundant and simply compile it as x << 2.
The Logic of the Redundant Mask
To understand why, consider an 8-bit integer .
| Step | Operation | Binary Representation | Note |
|---|---|---|---|
| 1 | Original | 1011 0111 | Initial value |
| 2 | x << 2 | 1101 1100 | Left shift; lowest 2 bits are now |
| 3 | Mask -4 | 1111 1100 | Two's complement; only clears lowest 2 bits |
| 4 | Result | 1101 1100 | Identical to Step 2 |
The bitwise AND has no effect because the shift already guaranteed the bits the mask intended to clear were already zero. But how does the C2 compiler actually prove this?
The Limitation of Signed Ranges
C2 optimizes code by proving constraints on variables. For instance, if the compiler can prove an array index falls within , it can entirely.
Historically, HotSpot tracked integer types using a signed range:
While useful for simple bounds, this abstraction is too blunt for bitwise operations. Consider a left shift: shifting can cause overflows and wrap-arounds, resulting in a massive range. The tightest signed interval for a shifted int might be:
This range is nearly the entire 32-bit space, providing the compiler with almost no actionable information. It can tell you if a number is "small," but it cannot tell you if a number is even or a multiple of four. That information exists in the bits, not the range.
Introducing KnownBits
To solve this, HotSpot introduced a way to track individual bit states. Instead of just a range, the compiler now tracks two 32-bit masks:
zeros: A bit is if that position is definitely 0.ones: A bit is if that position is definitely 1.
If a bit is in both masks, its value is unknown. Because a bit cannot be both and simultaneously, the following invariant must always hold:
zeros & ones == 0
Implementation Detail
In rangeinference.hpp, this is implemented as a lightweight template:
template class U
class KnownBits {
static_assert(U(-1) != U(0), "bit info should be unsigned");
public:
U _zeros;
U _ones;
bool is_satisfied_by(U v) const {
return (v & _zeros) == U(0) && (v & _ones) == _ones;
}
};
The is_satisfied_by method simply verifies if a value conforms to the known bit constraints.
Note: This approach is "non-relational." It tracks bits independently and does not record relationships (e.g., it cannot track "bit 3 is always equal to bit 5"). This independence is what keeps the bookkeeping computationally cheap.
Industry Context
This technique isn't unique to HotSpot; it's a common compiler pattern:
- LLVM: Uses a nearly identical
KnownBitsrepresentation. - GCC: Employs a similar "value plus uncertainty mask" in its conditional constant-propagation pass.
The Synergy: A Reduced Product
HotSpot now maintains two parallel descriptions of a number: the Range and the KnownBits. These two abstractions refine each other through a process known in compiler theory as a reduced product.
How they collaborate:
- Bits Range: If the
KnownBitsmask indicates the low two bits are zero (meaning the value is a multiple of 4), a range of can be tightened to . - Range Bits: If the range is , the compiler knows the top 24 bits must be zero, allowing it to update the
zerosmask.
This reconciliation happens in a function called canonicalize_constraints(), which runs whenever a new integer type is generated.
The Canonicalization Dance
To reach a stable state, the compiler performs the following steps:
- Check if the signed range implies any new
KnownBits. - Check if the
KnownBitsimply a tighter signed range. - Check if the unsigned range implies any new
KnownBits. - Repeat until no further refinements are possible (reaching a fixed point).
In type.hpp, a value is only considered part of a type if it satisfies three distinct constraints:
- Signed Range: and
- Unsigned Range: and
- Known Bits:
The inclusion of an unsigned range allows HotSpot to compile these checks into a single, efficient unsigned comparison.
Timeline & Credits
- JDK 26: Foundations laid by Quan Anh Mai (known as
merykitty) and Emanuel Peter (JDK-8315066). - JDK 27: Implementation of the logic that actually deletes redundant masks like
(x << 2) & -4.