HotSpot JIT编译器学会优化位掩码,使其编译后消失
速览
HotSpot JIT编译器通过改进位推理算法,能够识别不必要的位掩码运算,并在编译阶段将其优化移除。这项能力使生成的机器码更精简,尤其对位运算密集的Java程序有显著性能提升。该优化是JVM持续演进中的一项底层技术改进。
AI 深度解读
背景
现代编译器在优化代码时,需要深入理解程序中的数值属性。对于位运算这类底层操作,传统的优化方法(如范围分析)往往无法捕捉到关键信息。例如,表达式 (x << 2) & -4 在语义上等价于 x << 2,因为左移后低两位必定为0,而按位与 -4 只是清零低两位,没有实际效果。但编译器要自动识别这种优化,必须能推理出变量在特定位上的确定性知识。HotSpot JVM 的 C2 JIT 编译器在 JDK 26 和 27 中引入了基于已知位(known bits)的抽象,使得这类优化成为可能。本文基于 OpenJDK 开发者 Quan Anh Mai 和 Emanuel Peter 的贡献,详细解析了 C2 如何通过“范围+已知位”的联合类型来推理整数的位信息,并最终删除冗余的位运算。
核心内容
编译器对数值的了解:类型系统
当 C2 编译一个方法时,它对某个变量的值知道的是“一组可能的值”。编译器通常无法知道变量的确切运行时值,但可以证明该值被约束在某个集合内。如果约束足够紧,就可以改写代码。例如,证明数组索引始终在 [0, length) 范围内,就可以删除边界检查。C2 将这些“可能的值集合”存储为一个类型(type)。注意,这里的类型不是 Java 中的 int 或 long,而是一个更丰富的内部类型,携带一个范围(range)。
在很长一段时间里,C2 的整数类型仅包含一个有符号范围 [lo, hi],表示值在这个闭区间内。例如,x & 0xFF 的类型就是 [0, 255]。这个有符号范围简单实用,但对于我们的目标(优化 (x << 2) & -4)来说太弱了。
考虑 x << 2。如果 x 可以是任意值,那么 x << 2 的范围是什么?基本上几乎任何值都可能出现——左移可能溢出、绕回,产生巨大的正数和负数。覆盖结果的最紧有符号区间是 [Integer.MIN_VALUE, 2147483644],几乎等于整个 int 范围,对编译器几乎毫无用处。
但是,我们对于 x << 2 有一个非常具体的信息:无论 x 是什么,最低两位始终为零!范围无法表达这一点。[lo, hi] 可以说“值很小”,但无法说“值是偶数”,更不用说“值是4的倍数”。这些知识存在于位中,仅靠范围无法表达。
引入已知位(Known Bits)
解决方案:在范围旁边,额外跟踪每个位的已知信息。对于 32 位整数,用两个额外的 32 位掩码与类型一起传递:
_zeros:第 i 位为 1 表示“该位一定为 0”。_ones:第 i 位为 1 表示“该位一定为 1”。- 未知的位在两个掩码中均为 0。一个位不可能同时为 0 和 1,因此不变量
_zeros & _ones == 0必须始终成立。
在 C2 中,这由 rangeinference.hpp 中的几十行代码实现(模板类 KnownBits):
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;
}
};
is_satisfied_by 检查给定数值 v 是否被掩码允许。掩码绝不应排除可能实际出现的值,C2 的测试和内部检查会验证这一点。
现在每个位处于三种状态之一:已知-0、已知-1、未知。x << 2 的类型如下(. 表示未知):
bit: 31 ... 2 1 0
x: . . . . . . . . . . . . . . . . . . . (x 完全未知)
x<<2: . . . . . . . . . . . . . . . . . 0 0 (低两位:已知零!)
因此 x << 2 的 _zeros = 0b11,其余位未知。编译器现在知道该数是 4 的倍数。
这种数据结构并非 HotSpot 独有。LLVM 的 KnownBits 与 C2 的表示最接近(零掩码和一位掩码,满足 Zero & One == 0 不变量);GCC 在其条件常量传播阶段也跟踪同样的信息(一个值加一个不确定性掩码)。本质都是同一个思想:一种廉价、非关系型、逐位的整数抽象。
非关系型(Non-relational):该抽象独立跟踪每一位,从不记录位之间的关系(例如“位3始终等于位5”)。这种独立性使其廉价——只需常数级别的逐位簿记。
两个视图相互精化
现在,我们同时拥有一个数的两种不同描述:范围和已知位。每种描述都知道对方不知道的信息,并且它们可以互相教导:
- 位信息教给范围:如果已知位说低两位为零,而范围当前是
[5, 41],那么 5、6、7、41 的低两位非零,不可能出现,因此范围收紧为[8, 40](内部最接近的4的倍数)。 - 范围教给位信息:如果范围是
[0, 200],那么从 0 到 200 的所有数的高 24 位都是 0,因此这些位并非未知,而是已知-0,位信息记录下它们。
这种“组合两种抽象并让它们相互精化”的模式在编译器文献中有一个
