Skip to content

G1 barrier优化 #54

Description

@lfeng14
子项 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-mnemonicsreactors;但不能说四项都受益,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

优化后,在能够证明安全时变成:

store left
store right

它主要改善对象分配密集、构造函数字段较多的工作负载。实测 par-mnemonics 中位提升约 5.76%,reactors 提升约 1.59%。

分:为什么这些 barrier 可以省

1. 为什么 pre barrier 多余

G1 的 pre barrier 用于 SATB 并发标记:覆盖一个旧引用之前,需要把旧值记录下来,防止正在标记的对象丢失。

但新对象的引用字段在分配后都是零值:

field.oldValue == null

第一次初始化字段时,不存在需要记录的旧引用,因此 pre barrier 没有意义。

不过,如果同一个字段已经写过一次:

n.left = a;
n.left = b;

第二次写会覆盖 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:

n.left = a;
n.right = b;

leftright 已证明不别名,两次都属于首次初始化,可以全部省掉。

而:

n.left = a;
n.left = b;

第二次地址可能别名,因此必须保留 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-mnemonicsreactors 收益稳定。
  • page-rank 基本持平。
  • neo4j-analytics 没有明显受益,存在小幅回退。

因此它是一个有价值但 workload-sensitive 的优化:实现机制成立,正确性边界保守,值得继续保留和完善,但不应宣称所有应用都会提升。

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions