Когда компилятор C2 видит (x << 2) & -4, он может просто оставить x << 2 — битовая маска не нужна. Раньше он так не умел, потому что отслеживал только диапазон значений переменной, а диапазон сдвинутого числа слишком широкий и ничего не говорит о младших битах. Но C2 знает: после сдвига влево на 2 нижние два бита всегда нулевые. Эту информацию не выразить через [lo, hi].
Решили проблему просто: рядом с диапазоном компилятор теперь хранит две маски — _zeros и _ones. В них отмечено, какие биты точно равны нулю, а какие — единице. Остальные биты считаются неизвестными. Например, у x << 2 маска _zeros будет содержать единицы на позициях двух младших битов — значит, они гарантированно нулевые. Такая структура есть и в других компиляторах: в LLVM она называется KnownBits, в GCC отслеживается похожая маска. В HotSpot основа заложена в JDK 26 (тикеты JDK-8315066), а сама оптимизация, убирающая бесполезный AND, заработала в JDK 27 (тикет JDK-8380475).
Но одного набора битов мало. У компилятора есть ещё диапазон, и они могут уточнять друг друга. Если диапазон говорит, что значение от 5 до 41, а биты утверждают, что младшие два бита нулевые, то диапазон сужается до [8, 40] — все некратные четырём числа отбрасываются. И наоборот: если диапазон мал, верхние биты становятся известными нулями. Этот процесс взаимного уточнения называется canonicalize_constraints(). Он повторяется по циклу, пока либо не сойдётся, либо не станет пустым. Так как битов всего 64, цикл гарантированно завершается.
Правила для самих операций тоже прописаны. Для AND: единица на выходе только если единица в обоих операндах, ноль — если ноль хотя бы в одном. Для сдвига влево: маски тоже сдвигаются, а освободившиеся младшие биты помечаются как нулевые. Эти transfer functions прописаны прямо в коде C2.
Самый наглядный результат — в сгенерированном машинном коде. На JDK 24 выражение (x << 2) & -4 превращалось в shl и отдельный and. На JDK 27 остаётся только lea — никакого лишнего AND. Но если заменить маску на -8, которая очищает ещё и третий бит, компилятор оставит and — он не может доказать, что этот бит точно ноль.
Корректность всех этих новых правил проверили исчерпывающим тестированием на 4-битных целых числах. Через шесть вложенных циклов перебрали все возможные комбинации диапазонов и масок — каждую точку в этом маленьком пространстве значений. Более широкие типы проверялись уже случайными тестами.
Эту оптимизацию в C2 реализовали разработчики Quan Anh Mai (он же merykitty), Emanuel Peter и Ashay Rane. Теперь компилятор действительно понимает, что (x << 2) & -4 — это просто x << 2, и не тратит на маску ни одной инструкции.