Skip to content

概念:Stub、SharedRuntime 还是 LLVM intrinsic #49

Description

@lfeng14

可以先把三者理解成:它们都是实现 Math.log() 等操作的不同落地方式,区别在于“代码由谁提供、什么时候决定、优化空间有多大”。

先看调用链

Java 代码:

double result = Math.log(x);

Jeandle 识别出这是一个 intrinsic 候选后,可以选择:

Math.log(x)
    ├── StubRoutines:调用 HotSpot 针对当前 CPU 准备的专用代码
    ├── SharedRuntime:调用 HotSpot 提供的通用 C/C++ 实现
    └── LLVM intrinsic:在 IR 中描述“这是 log”,交给 LLVM 决定如何实现

1. Stub / StubRoutines

Stub 可以理解成 JVM 提前准备好的“小段专用机器代码”。

例如:

StubRoutines::dlog()
StubRoutines::dexp()
StubRoutines::dpow()

这些入口通常由 HotSpot 根据当前 CPU 架构和能力,在 JVM 启动阶段生成或选择。

编译后的业务代码直接调用它:

业务代码 → StubRoutines_dlog → 平台优化的 log 实现

它的特点是:

  • 通常针对特定 CPU 架构优化。
  • 已经与 HotSpot 的运行环境集成。
  • 调用开销比完整的 Java 方法调用路径低。
  • 不一定每个平台都提供对应 Stub。
  • 函数地址可能为 nullptr,所以必须有 fallback。

可以类比为:

JVM 自带的一套平台优化汇编函数库。

这里的 Stub 不一定意味着只有几条指令。数学函数实现可能包含范围规约、查表、多项式逼近和特殊值处理,只是以一个专用入口的形式暴露给编译代码。

2. SharedRuntime

SharedRuntime 是 HotSpot 提供的一组通用运行时辅助函数。

例如:

SharedRuntime::dlog
SharedRuntime::dlog10
SharedRuntime::dexp
SharedRuntime::dpow

调用关系是:

业务代码 → SharedRuntime::dlog → 通用数学实现

它主要承担 fallback 的角色:

if (StubRoutines::dlog() != nullptr) {
    call StubRoutines::dlog();
} else {
    call SharedRuntime::dlog();
}

它的特点是:

  • 实现在多个架构之间共享。
  • 不依赖某个平台必须存在专用 Stub。
  • 更关注通用性和正确性。
  • 通常没有专用 Stub 那么贴近具体 CPU。
  • 对编译器来说依然是一次外部函数调用。

可以类比为:

JVM 自带的通用 C/C++ runtime 函数库。

这里的 “Shared” 主要是相对于架构专用实现而言:不同 CPU 后端可以共同使用这套运行时入口。

3. LLVM intrinsic

LLVM intrinsic 不是一段已经确定的函数实现,而是 LLVM IR 中具有特殊语义的操作。

例如:

%result = call double @llvm.log.f64(double %x)

类似的还有:

@llvm.log10.f64
@llvm.exp.f64
@llvm.pow.f64

它表达的是:

这里需要计算一个 double 类型的 log,请 LLVM 根据目标平台、优化级别和语义约束决定最终怎么实现。

LLVM 后续可能将它:

  • 常量折叠;
  • 与其他表达式一起优化;
  • 映射到目标平台实现;
  • 转换成数学库调用;
  • 在结果未使用时直接删除。

例如:

double x = Math.log(1.0);

LLVM可能在编译期直接计算结果:

Math.log(1.0) → 0.0

而:

Math.log(x); // 结果未使用

如果操作没有必须保留的副作用,LLVM可能直接删除。

它的特点是:

  • 数学语义对 LLVM 可见。
  • 优化空间通常比普通外部函数调用更大。
  • 最终不一定变成一条 CPU 指令。
  • 可能仍然被降低为 libm 调用。
  • 正确性受 LLVM intrinsic 语义、fast-math 配置和目标实现影响。

可以类比为:

告诉 LLVM“我要做什么”,而不是直接指定“调用哪一段代码”。

三者的核心区别

维度 | StubRoutines | SharedRuntime | LLVM intrinsic -- | -- | -- | -- 由谁提供 | HotSpot 平台后端 | HotSpot 通用 runtime | LLVM 当前是否已有具体实现 | 有 | 有 | IR 阶段未完全确定 是否通常与架构相关 | 强相关 | 相对通用 | 由 LLVM 目标后端决定 IR 中的表现 | 外部函数调用 | 外部函数调用 | 特殊 LLVM IR 调用 LLVM 是否理解数学语义 | 通常视为普通调用 | 通常视为普通调用 | 理解 优化空间 | JVM/Stub 内部优化 | 相对有限 | 较大 可用性 | 可能不存在 | 用作通用 fallback | 取决于 LLVM 支持 主要价值 | 平台性能 | 兼容性和正确性 | 编译期及跨表达式优化

为什么同时需要三者?

因为它们解决的问题不同。

Stub 解决平台性能

如果 x86 已经有一个经过优化和验证的 dlog Stub,直接复用通常比重新实现更可靠。

SharedRuntime 解决可用性

如果 AArch64 或其他平台还没有相应 Stub,不能让编译器因此无法编译 Math.log。此时退回通用 runtime。

LLVM intrinsic 提供另一条优化路径

当希望让 LLVM看到完整数学表达式时,可以生成:

llvm.log.f64

这样 LLVM才有机会做常量折叠和表达式级优化。

这个 commit 具体怎么选择?

JeandleUseHotspotIntrinsics 控制。

开启时:

JeandleUseHotspotIntrinsics = true
    │
    ├── Stub 存在 → StubRoutines
    └── Stub 不存在 → SharedRuntime

关闭时:

JeandleUseHotspotIntrinsics = false
    │
    └── LLVM intrinsic

伪代码是:

if (JeandleUseHotspotIntrinsics) {
    if (StubRoutines::dlog() != nullptr) {
        result = call StubRoutines::dlog(x);
    } else {
        result = call SharedRuntime::dlog(x);
    }
} else {
    result = llvm.log(x);
}

面试中最简洁的说法

StubRoutines 是 HotSpot 针对特定 CPU 准备的专用代码;SharedRuntime 是没有专用 Stub 时使用的通用运行时实现;LLVM intrinsic 则是在 IR 中保留数学语义,让 LLVM决定最终生成专用指令、代码序列还是库调用。这个 commit 同时支持两套策略:优先复用 HotSpot 的 Stub并用 SharedRuntime 兜底,或者直接交给 LLVM lowering。

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