OpenJDK 的 RISC-V 生态实践:从版本反合到 C2 后端优化

简介: 为什么在 RISC-V 生态中谈 OpenJDK?

编者按:随着 RISC-V 向服务器级芯片演进,Java 作为服务端生态的核心语言,其运行时 OpenJDK 在新架构上的成熟度直接决定着 Java 服务落地的广度与深度。中兴通讯作为龙蜥社区副理事长单位,围绕 RISC-V 架构下 Java 基础软件栈的完善持续投入,重点推进 OpenJDK 在编译器后端、向量化能力和长期支持版本适配等方向的技术实践,并将相关工程经验与优化成果反馈到社区生态中,助力龙蜥社区推动 RISC-V 编译器相关技术的持续演进与规模化落地。在RISC-V 编译器生态和性能优化 MeetUp上,来自中兴通讯的张诗烩分享了《OpenJDK RISC-V 生态实践》主题演讲。他围绕 OpenJDK 在 RISC-V 架构上的工程化实践展开,重点介绍了 OpenJDK master 分支中 RISC-V 相关改进反合到 OpenJDK 21 的过程,并结合当前针对 RVV 后端冗余 vset 指令的优化工作,分析了 RISC-V 向量化代码生成中的性能问题与改进思路。以下为本次演讲全文。

为什么在 RISC-V 生态中谈 OpenJDK?

在正式展开技术细节之前,我想先回答一个问题:为什么我们需要在 RISC-V 生态里专门讨论 OpenJDK?

我认为主要有两个方面的原因。一方面,当前 RISC-V 正走向服务器级别的芯片,而 Java 在服务器生态领域里占据着非常重要的作用——企业级中间件、大数据平台、微服务框架背后几乎都是 JVM 在支撑。OpenJDK 在 RISC-V 上的成熟度,将直接影响 Java 服务生态在该架构上的落地。另一方面,OpenJDK 本身也是一个非常综合的软件栈,集成了 JIT 编译器、垃圾回收器、向量化库、JNI 原生接口等大量底层能力。如果 OpenJDK 能在 RISC-V 架构上高性能地运行,也从一定层面反映出 RISC-V 生态的成熟度。它既是一个“用户”,也是一个“标尺”。

从版本演进的角度看,OpenJDK 对 RISC-V 的支持程度呈现出一个逐步上升的趋势。对于 OpenJDK 8、OpenJDK 11 早期 LTS 版本,上游的支持程度非常有限,主要依靠下游厂商的反合与维护来推进。真正的转折点发生在 OpenJDK 19 的 JEP 422——从这一节点开始,RISC-V 正式进入 OpenJDK 上游主线。当前 OpenJDK 主线已基本实现对 RVA23 Profile 的支持,并合入了大量向量、加解密优化等后端能力。

不过,当前我们仍然面临一个现实矛盾:主线分支对 RISC-V 的支持在不断演进,但大量用户仍在使用存量的 LTS 版本,这些版本对 RISC-V 的支持远远不够,而这将是 OpenJDK 在 RISC-V 生态上必须直面的挑战。

当前问题:从“支持可用”到“性能成熟”

从上述介绍可以看到,OpenJDK 主线已经完成了对 RISC-V 的基本支持。所以当下的问题已经从“支持可用”到“性能成熟”。我认为至少有三个方面的问题有待解决:

第一,LTS 版本的反合与维护。目前有大量用户仍在使用 OpenJDK 8、OpenJDK 11 等旧版本,如何为这些用户提供一个高性能、可维护的 OpenJDK 版本,将直接决定 OpenJDK 在 RISC-V 上的使用范围。

第二,后端优化,尤其是 C2 编译器的优化。 它决定了 OpenJDK 在 RISC-V 上的性能能否与 x86、Arm 等主流成熟架构对齐。

第三,Native 库的适配优化。 许多 Java 应用底层依赖 Native 库(比如 OpenSSL、FFmpeg 等),这些库在 RISC-V 上的适配质量,会直接决定 Java 应用在 RISC-V 架构上的迁移成本。

我们在 OpenJDK 生态上做的实际工作

中兴通讯在 OpenJDK 做的工作可以分为两个方面:OpenJDK 21 反合与主线分支的优化推进。在 OpenJDK 21 的反合工作中,我们分析筛选出主线中 40 余条关键 PR,共计一万多行代码合入到 OpenJDK 21,并完成了反合代码的功能与性能验证。SPECjvm 2008 的测试结果显示,反合后整体性能提升了约 4 %。另一方面,我们也在积极推进 OpenJDK 主线分支的优化。目前共计开发了十多项特性,涵盖:

  • 扩展集补齐:让 OpenJDK 能调用更多 RISC-V 指令集能力;
  • 加解密算法优化:利用 RISC-V 向量扩展加速 SM/AES 等算法;
  • C2 后端优化:包括本文重点分享的冗余 vset 指令消除方案。

SPECjvm 测试结果显示,相比开源版本,我们优化后的版本整体性能提升了约 10%。

深度剖析:如何消除 C2 产生的冗余 vset 指令

下面我将具体介绍中兴通讯在 OpenJDK RISC-V 上做的优化,聚焦于如何消除 C2 编译器产生的冗余 vset 指令。

RISC-V 向量扩展(RVV)采用向量长度无关(VLA)模型,使得同一份向量代码无需针对不同硬件的寄存器长度进行修改即可直接执行。为实现这一特性,RVV 要求在执行向量操作前通过 vset 指令配置执行状态,涵盖元素数量、元素宽度(sew)、寄存器分组(lmul)、掩码及尾部处理策略等参数。

尽管 vset 是向量操作的必要前提,但若被重复配置为相同状态,则会引入不必要的性能开销。例如在汇编序列中,首条 vset 因需初始化向量状态而不可或缺,但后续若出现配置完全相同的 vset 指令,便构成了冗余。

这类冗余指令主要从三个方面损害性能:首先,vset 会更新 vl、vtype 等 CSR 寄存器,冗余调用会频繁制造状态切换边界,干扰流水线调度;其次,作为真实指令,它会占用 fetch、decode、issue 等前端资源,在循环或核心计算上下文中更难隐藏延迟;最后,冗余指令还会拉长循环体代码,降低代码密度并增加 L1 Cache 压力。鉴于上述影响,在编译器层面消除冗余 vset 指令成为优化 RVV 程序性能的关键措施。

在 C2 编译器中,一个简单数字加法循环会被编译成 LoadVectorAddVectorStoreVector 等架构无关的中间表示,经过一系列图优化后,最终匹配到 RISC-V 后端的 MachNode 上,用于发射机器码。这个问题就出现在 MachNode 这一层。

在当前 OpenJDK 的实现中,为了保证每条向量指令都能正确执行,它在每个向量 MachNode 里都调用了一个 vset_helper 函数。这个函数会读取每个向量 MachNode 所需的向量状态配置,然后生成一条匹配的 vset 指令。由于这些 vset_helper 之间缺少统一的状态视图,每个向量 MachNode 都会生成一条 vset,冗余也就此产生。

冗余 vset 指令产生的根源在于编译器缺乏统一的状态视图。为构建这一视图,我们需要在编译器中将 vset 指令的状态显式化表达出来。以下图左侧所示,从 Java 代码到最终机器码的编译流程为例,我们可以从中识别出三个建立 vset 指令显式化的潜在切入点。其中,第一个切入点位于最终机器码发射阶段:在此阶段,编译器能够逐条读取待发射的指令,因此可以顺势记录下每条 vset 指令所配置的状态信息,从而为后续消除冗余奠定基础。第二个切入点可以选择在架构无关的中间表示层(IdealGraph )中进行,通过在该层设计专门的 vset IR 节点,我们便能够利用后续的图优化过程实现状态的有效传播。第三个切入点则位于 MatchNode 层,若在此处插入 vset 的 Match Node 节点,同样可以依托后续的控制流图完成优化。

策略一:在 vset_helper 中记录状态

由于每条 vset 指令最终都要通过 vset_helper 发射,我们可以在其中记录上一次 vset 的状态。如果当前要发射的状态与前一次一致,就跳过发射。本方案的优点是实现非常简单——最初用不到 100 行代码差不多实现了。而它的主要不足是在代码发射阶段,编译器只能以发射顺序看到指令;一旦指令全部进入 code buffer,后续就很难再做调整和修改。

策略二:IdealGraph 层优化

上述提到的第二种方案选择在架构无关的中间表示层( IdealGraph )中实施,即通过插入专门的 vset 节点,借助后续的图优化过程实现状态传播。该方案的优势在于优化介入点较少,且理论上具备最全局的视野,因此能够支持更为复杂的优化策略。然而,其显著缺点是将 RISC-V 特有的向量状态引入了平台无关的 IR 层,这会破坏中间表示的通用性,给后续的整体代码设计与维护带来极大的复杂度。

策略三:MachNode 层优化(最终选择)

在 MachNode 层插入 vset MachNode 节点。由于 MachNode 已经进入了 RISC-V 后端,在这一层做优化不会污染平台无关的 IdealGraph;同时控制流图等信息仍然存在,可以做相对复杂高级的优化。

我们最终选择了这一方案,并围绕显式化的 vset 节点设计了三种去冗余方法:

1. 块内去冗余。让编译器维护一个 currentState,记录当前基本块内的 vset 状态。每当遇到一个 vset 节点,就和 currentState 对比——相同则删除,不同则保留并更新状态。

2. 块间去冗余。为每个基本块配置 inStateoutState 两个状态。inState 描述进入基本块时编译器能确定的 vset 状态,outState 描述该基本块能传递给下一个基本块的状态。只有当所有前驱基本块的 outState 相同且有效时,才把当前基本块的 inState 设为该状态,从而实现基本块间的状态传播。

3. 循环外提。 在 C2 中,一个循环会被编译成“循环边(entry edge)”和“循环体”两部分。循环体会被反复执行,而循环边一般只执行一次。我们把循环体内满足外提条件的 vset 指令提到循环边里——由于循环边和循环体属于两个不同的基本块,后续再用块间去冗余方法就能删除循环体里的冗余 vset

MachNode 层优化验证结果显示,在正确性方面,我们用 JTReg 测试在 QEMU 与真实 RISC-V 硬件环境上跑通了全部相关用例。在性能方面,SPECjvm 及相关 benchmark 测试显示,优化带来了比较明显的性能提升。

总结

RISC-V 生态的成熟不是一家公司的独角戏,而是整个社区持续共建的结果。未来,中兴通讯也将依托龙蜥社区这一开放平台,与广大社区开发者和产业伙伴携手参与 RISC-V 基础软件生态共建,围绕 OpenJDK 编译器优化、版本适配、性能验证和应用迁移等方向沉淀更多实践成果,共同推动 RISC-V Java 软件栈从“可用”走向“好用”,为更多业务场景在新架构上的落地提供支撑。

本次 MeetUp  PPT 、视频回放已上线龙蜥官网,欢迎点击查看:

PPT 下载链接

https://docs.openanolis.cn/document/detail/rpzigrnb

视频回放:

https://openanolis.cn/video/#1658313779883670231

—— 完 ——

相关文章
|
1月前
|
人工智能 文字识别 JavaScript
AI 测试提效 | 脚本能跑但总挂?分享我的 ui-testscript-enhancer + Skill UI 自动化健壮性增强方案
本文介绍`ui-testscript-enhancer`技能:自动为AI生成的UI测试脚本注入六大健壮性能力——智能等待、弹窗处理、iframe/Shadow DOM穿透、异常重试、失败截图录屏及验证码识别(集成ddddocr),5分钟将“能跑”的脚本升级为CI-ready的“跑得稳”生产级脚本。
253 2
AI 测试提效 | 脚本能跑但总挂?分享我的 ui-testscript-enhancer + Skill UI 自动化健壮性增强方案
|
1月前
|
人工智能 运维 自然语言处理
最新版通义千问(Qwen3.8-Max)功能介绍
作为通义千问系列迄今规模最大、性能最强的旗舰模型,Qwen3.8-Max凭借2.4万亿总参数的MoE混合专家架构、100万Token上下文窗口与原生多模态能力,实现了从“辅助工具”到“自主智能体”的跨越。它不仅在代码工程、专业办公、复杂推理等核心领域实现跨越式升级,更以端到端交付生产级成果的能力,成为面向智能体时代的通用AI基座,为个人开发者、企业团队与科研机构提供前所未有的AI生产力支撑。
446 1
|
4月前
|
运维 监控 网络协议
排障本身不慢,MTTR却一直降不下来?时间都耗在这三个流程断点上
本文剖析MTTR居高不下的三大非技术断点:信息传递失真、工单推进停滞、跨部门信号断档。指出问题本质是流程链路断裂,而非排障能力不足,并给出低成本、可落地的三步改进方案:加推进超时提醒、标准化服务台字段、打通业务上报与端到端探测。(239字)
538 1
排障本身不慢,MTTR却一直降不下来?时间都耗在这三个流程断点上
|
1月前
|
人工智能 自然语言处理 运维
企业培训平台多语言内容生产架构设计:从文档翻译到视频课程的全链路国际化实践
在全球化业务扩张的背景下,出海企业面临着培训内容的多语言适配挑战。传统的多语言内容生产模式存在效率低、版本管理混乱、运维成本高等问题。本文将分享企学宝平台在AI多语能力建设中的技术实践,重点介绍如何通过AI文档翻译和智能制课系统,实现从知识文档到视频课程的全链路多语言生产与管理。
127 1
|
2月前
|
安全 Java 数据安全/隐私保护
|
1月前
|
人工智能 弹性计算 运维
STAROps 主机智能巡检:给你的 ECS 请个 24 小时在线的 AI 医生
STAROps 主机智能巡检:给你的 ECS 请个 24 小时在线的 AI 医生。

热门文章

最新文章