| 子项 |
Pair 1 |
Pair 2 |
Pair 3 |
三对中位收益 |
判断 |
| page-rank |
+4.11% |
-0.42% |
-1.17% |
-0.42% |
基本持平、波动较大 |
| par-mnemonics |
+5.76% |
+6.56% |
+2.87% |
+5.76% |
稳定正收益 |
| neo4j-analytics |
-0.80% |
+0.10% |
-3.24% |
-0.80% |
偏负,不受益 |
| reactors |
+1.59% |
+0.83% |
+3.09% |
+1.59% |
稳定正收益 |
| 四项几何平均 |
+2.70% |
+1.81% |
+0.42% |
+1.81% |
三对均为正 |
正数表示耗时下降。
结论:这个点值得保留继续推进,主要收益来自 par-mnemonics 和 reactors;但不能说四项都受益,page-rank 接近噪声,neo4j-analytics 有约 0.8% 的中位回退。
实现内容:
- 支持新实例和新对象数组初始化写。
- 仅在对象未发布、无中间 safepoint、写操作不会绕过下一次分配重复执行时消除 barrier。
- 首次字段写可同时消除 pre/post barrier。
- 同字段二次写保留 SATB pre barrier。
- 直接复用
-XX:+/-ReduceInitialCardMarks,没有修改 C3 编译线程并发度。 - 生产 IR 验证中,两次完整 G1 pre/post barrier 已简化为两条字段 store。
验证结果:
- LLVM 定向测试通过。
- JDK release images 构建通过。
- 3 个 GC jtreg 测试通过。
- 四项 Renaissance smoke 全部通过。
- 正式测试 24/24 个 JVM 成功,192 条迭代记录完整。
- 固定 NUMA 2、8 iterations、取 3–7 中位数、三对反序;未使用
CICompilerCount。
实现位于 [InsertGCBarriers.cpp (line 35)](/home/luofeng/jdk21-c3-jit/jeandle-llvm/llvm/lib/Transforms/Jeandle/InsertGCBarriers.cpp:35),测试见 [reduce-initial-card-marks.ll (line 1)](/home/luofeng/jdk21-c3-jit/jeandle-llvm/llvm/test/Jeandle/reduce-initial-card-marks.ll:1),完整数据汇总见 [ricm-ab-20260805_1457-summary.md](/home/luofeng/jdk21-c3-jit/results/ricm-ab-20260805_1457-summary.md)。
---
## 总:它解决什么问题
ReduceInitialCardMarks 的核心目标是:
新对象刚分配出来、字段仍是初始零值时,避免为字段初始化写入生成没有实际作用的 GC pre/post barrier。
C3 原来把所有 Java 堆引用写入都统一处理,即使是构造函数里的初始化写,也会生成完整 G1 barrier:
Node n = new Node();
n.left = left;
n.right = right;
原来的效果近似:
pre barrier
store left
post barrier
pre barrier
store right
post barrier
优化后,在能够证明安全时变成:
它主要改善对象分配密集、构造函数字段较多的工作负载。实测 par-mnemonics 中位提升约 5.76%,reactors 提升约 1.59%。
分:为什么这些 barrier 可以省
1. 为什么 pre barrier 多余
G1 的 pre barrier 用于 SATB 并发标记:覆盖一个旧引用之前,需要把旧值记录下来,防止正在标记的对象丢失。
但新对象的引用字段在分配后都是零值:
第一次初始化字段时,不存在需要记录的旧引用,因此 pre barrier 没有意义。
不过,如果同一个字段已经写过一次:
第二次写会覆盖 a,这时不能再省 pre barrier。因此当前设计是:
第一次写 left:省 pre + post
第二次写 left:保留 pre,只省 post
2. 为什么 post barrier 多余
G1 的 post barrier 主要负责维护 remembered set/card table,例如记录老年代对象指向年轻代对象的引用。
正常 TLAB 分配出来的新对象本身就在年轻代。年轻对象字段初始化不需要记录“老对象指向年轻对象”的关系,因此 post barrier 可以省掉。
但新对象也可能走慢分配,例如:
- TLAB 空间不足
- 大对象或 humongous object
- 非年轻代分配路径
这些情况下不能简单假设对象一定在年轻代。好在 Jeandle 的慢分配路径已经调用了:
SharedRuntime::on_slowpath_allocation_exit(current);
GC 会在这里执行必要的补偿:
- 年轻对象:不需要处理。
- 非年轻对象:使对应区域/card 失效或延迟 card mark。
也就是说,之前 runtime 补偿机制已经存在,只是 C3 编译器还没有据此消除初始化 barrier。
分:编译器如何识别安全的初始化写
实现放在 InsertGCBarriers 阶段。此时:
- 构造函数通常已经内联。
jeandle.new_instance / jeandle.new_array 还没有被底层分配代码替换。
- 编译器仍能看清“分配对象 → 初始化字段”的关系。
主要分为四步。
第一步:识别新对象来源
从字段 store 的地址反向追踪:
%obj = call @jeandle.new_instance(...)
%field = getelementptr ..., %obj
store %value, %field
目前支持追踪:
getelementptr
bitcast
addrspacecast
freeze
最终必须追踪到:
jeandle.new_instance
jeandle.new_array
如果经过复杂 PHI、未知指针转换或无法确认来源,就放弃优化。
第二步:确认对象仍然是 fresh object
必须同时满足:
- 分配指令支配字段 store。
- 分配到 store 之间没有可能 safepoint 的调用。
- 对象没有被发布到外部。
- store 不会在没有重新分配对象的情况下循环执行。
例如下面情况不会优化:
Node n = new Node();
possiblySafepoint();
n.left = value;
因为 safepoint 后对象可能已经晋升或被 GC 移动,不能再按“刚分配的年轻对象”处理。
下面也不会优化:
Node n = new Node();
global = n; // 对象已经发布
n.left = value;
发布后其他线程或未知代码可能访问、修改对象,编译器不再认为字段保持初始状态。
循环重复写也会拒绝:
Node n = new Node();
while (...) {
n.left = value;
}
第一次可能是初始化,后续迭代已经不是。
第三步:分别判断 pre 和 post barrier
pre 和 post barrier 的条件不同,不能简单一起删除。
对于 post barrier:
对象仍 fresh
→ 可以删除 post barrier
对于 pre barrier:
对象仍 fresh
并且此前没有可能写过同一字段
→ 可以删除 pre barrier
实现会使用 LLVM Alias Analysis 检查此前的字段 store:
left 和 right 已证明不别名,两次都属于首次初始化,可以全部省掉。
而:
第二次地址可能别名,因此必须保留 pre barrier。
第四步:无法证明就回退
这个实现采用保守策略:
只有明确证明安全才删除 barrier;任何未知情况都维持原有完整 barrier。
会触发回退的情况包括:
- 中间存在函数调用。
- 对象已经逃逸或发布。
- 指针来源无法追踪。
- 复杂 PHI/select 指针。
- 循环可能重复执行同一字段写。
- 未知的原子操作或指针使用。
- 之前存在可能别名的字段写。
所以它不会改变 Java 内存语义,只减少可以确定为冗余的 GC 工作。
开关与流水线设计
没有新增独立 JVM 产品开关,而是直接复用 HotSpot 已有的:
-XX:+ReduceInitialCardMarks
-XX:-ReduceInitialCardMarks
C3 创建 LLVM pipeline 时把这个开关传给 InsertGCBarriers:
HotSpot ReduceInitialCardMarks
↓
Jeandle PipelineOptions
↓
InsertGCBarriers
↓
识别 fresh initializing store
↓
决定是否插入 pre/post barrier
这样编译器消除行为和 runtime 慢分配补偿由同一个开关控制:
- 开启:编译器省 barrier,runtime 对特殊慢分配补偿。
- 关闭:编译器插入完整 barrier,runtime 不需要补偿。
这次实现没有修改 CICompilerCount 或任何 C3 编译线程并发度。
总:这个特性的定位
它本质上是一个“新对象初始化写特化”:
- 不是删除所有 GC barrier。
- 只优化明确来自新分配对象的首次字段初始化。
- 利用字段零初始化省 pre barrier。
- 利用年轻代分配和慢路径补偿省 post barrier。
- 通过支配关系、CFG、逃逸检查和别名分析保证安全。
- 证明失败时保持原行为。
当前收益也符合这个定位:
- 对象创建和构造字段写较密集的
par-mnemonics、reactors 收益稳定。
page-rank 基本持平。
neo4j-analytics 没有明显受益,存在小幅回退。
因此它是一个有价值但 workload-sensitive 的优化:实现机制成立,正确性边界保守,值得继续保留和完善,但不应宣称所有应用都会提升。
正数表示耗时下降。
结论:这个点值得保留继续推进,主要收益来自
par-mnemonics和reactors;但不能说四项都受益,page-rank接近噪声,neo4j-analytics有约 0.8% 的中位回退。实现内容:
-XX:+/-ReduceInitialCardMarks,没有修改 C3 编译线程并发度。验证结果:
CICompilerCount。实现位于 [InsertGCBarriers.cpp (line 35)](/home/luofeng/jdk21-c3-jit/jeandle-llvm/llvm/lib/Transforms/Jeandle/InsertGCBarriers.cpp:35),测试见 [reduce-initial-card-marks.ll (line 1)](/home/luofeng/jdk21-c3-jit/jeandle-llvm/llvm/test/Jeandle/reduce-initial-card-marks.ll:1),完整数据汇总见 [ricm-ab-20260805_1457-summary.md](/home/luofeng/jdk21-c3-jit/results/ricm-ab-20260805_1457-summary.md)。
--- ## 总:它解决什么问题ReduceInitialCardMarks的核心目标是:C3 原来把所有 Java 堆引用写入都统一处理,即使是构造函数里的初始化写,也会生成完整 G1 barrier:
原来的效果近似:
优化后,在能够证明安全时变成:
它主要改善对象分配密集、构造函数字段较多的工作负载。实测
par-mnemonics中位提升约 5.76%,reactors提升约 1.59%。分:为什么这些 barrier 可以省
1. 为什么 pre barrier 多余
G1 的 pre barrier 用于 SATB 并发标记:覆盖一个旧引用之前,需要把旧值记录下来,防止正在标记的对象丢失。
但新对象的引用字段在分配后都是零值:
第一次初始化字段时,不存在需要记录的旧引用,因此 pre barrier 没有意义。
不过,如果同一个字段已经写过一次:
第二次写会覆盖
a,这时不能再省 pre barrier。因此当前设计是:2. 为什么 post barrier 多余
G1 的 post barrier 主要负责维护 remembered set/card table,例如记录老年代对象指向年轻代对象的引用。
正常 TLAB 分配出来的新对象本身就在年轻代。年轻对象字段初始化不需要记录“老对象指向年轻对象”的关系,因此 post barrier 可以省掉。
但新对象也可能走慢分配,例如:
这些情况下不能简单假设对象一定在年轻代。好在 Jeandle 的慢分配路径已经调用了:
SharedRuntime::on_slowpath_allocation_exit(current);GC 会在这里执行必要的补偿:
也就是说,之前 runtime 补偿机制已经存在,只是 C3 编译器还没有据此消除初始化 barrier。
分:编译器如何识别安全的初始化写
实现放在
InsertGCBarriers阶段。此时:jeandle.new_instance/jeandle.new_array还没有被底层分配代码替换。主要分为四步。
第一步:识别新对象来源
从字段 store 的地址反向追踪:
目前支持追踪:
getelementptrbitcastaddrspacecastfreeze最终必须追踪到:
jeandle.new_instancejeandle.new_array如果经过复杂 PHI、未知指针转换或无法确认来源,就放弃优化。
第二步:确认对象仍然是 fresh object
必须同时满足:
例如下面情况不会优化:
因为 safepoint 后对象可能已经晋升或被 GC 移动,不能再按“刚分配的年轻对象”处理。
下面也不会优化:
发布后其他线程或未知代码可能访问、修改对象,编译器不再认为字段保持初始状态。
循环重复写也会拒绝:
第一次可能是初始化,后续迭代已经不是。
第三步:分别判断 pre 和 post barrier
pre 和 post barrier 的条件不同,不能简单一起删除。
对于 post barrier:
对于 pre barrier:
实现会使用 LLVM Alias Analysis 检查此前的字段 store:
left和right已证明不别名,两次都属于首次初始化,可以全部省掉。而:
第二次地址可能别名,因此必须保留 pre barrier。
第四步:无法证明就回退
这个实现采用保守策略:
会触发回退的情况包括:
所以它不会改变 Java 内存语义,只减少可以确定为冗余的 GC 工作。
开关与流水线设计
没有新增独立 JVM 产品开关,而是直接复用 HotSpot 已有的:
C3 创建 LLVM pipeline 时把这个开关传给
InsertGCBarriers:这样编译器消除行为和 runtime 慢分配补偿由同一个开关控制:
这次实现没有修改
CICompilerCount或任何 C3 编译线程并发度。总:这个特性的定位
它本质上是一个“新对象初始化写特化”:
当前收益也符合这个定位:
par-mnemonics、reactors收益稳定。page-rank基本持平。neo4j-analytics没有明显受益,存在小幅回退。因此它是一个有价值但 workload-sensitive 的优化:实现机制成立,正确性边界保守,值得继续保留和完善,但不应宣称所有应用都会提升。