可以先把三者理解成:它们都是实现 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可能在编译期直接计算结果:
而:
如果操作没有必须保留的副作用,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才有机会做常量折叠和表达式级优化。
这个 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。
可以先把三者理解成:它们都是实现
Math.log()等操作的不同落地方式,区别在于“代码由谁提供、什么时候决定、优化空间有多大”。先看调用链
Java 代码:
Jeandle 识别出这是一个 intrinsic 候选后,可以选择:
1. Stub / StubRoutines
Stub 可以理解成 JVM 提前准备好的“小段专用机器代码”。
例如:
这些入口通常由 HotSpot 根据当前 CPU 架构和能力,在 JVM 启动阶段生成或选择。
编译后的业务代码直接调用它:
它的特点是:
nullptr,所以必须有 fallback。可以类比为:
这里的 Stub 不一定意味着只有几条指令。数学函数实现可能包含范围规约、查表、多项式逼近和特殊值处理,只是以一个专用入口的形式暴露给编译代码。
2. SharedRuntime
SharedRuntime是 HotSpot 提供的一组通用运行时辅助函数。例如:
调用关系是:
它主要承担 fallback 的角色:
它的特点是:
可以类比为:
这里的 “Shared” 主要是相对于架构专用实现而言:不同 CPU 后端可以共同使用这套运行时入口。
3. LLVM intrinsic
LLVM intrinsic 不是一段已经确定的函数实现,而是 LLVM IR 中具有特殊语义的操作。
例如:
类似的还有:
它表达的是:
LLVM 后续可能将它:
例如:
LLVM可能在编译期直接计算结果:
而:
如果操作没有必须保留的副作用,LLVM可能直接删除。
它的特点是:
可以类比为:
三者的核心区别
为什么同时需要三者?
因为它们解决的问题不同。
Stub 解决平台性能
如果 x86 已经有一个经过优化和验证的
dlogStub,直接复用通常比重新实现更可靠。SharedRuntime 解决可用性
如果 AArch64 或其他平台还没有相应 Stub,不能让编译器因此无法编译
Math.log。此时退回通用 runtime。LLVM intrinsic 提供另一条优化路径
当希望让 LLVM看到完整数学表达式时,可以生成:
这样 LLVM才有机会做常量折叠和表达式级优化。
这个 commit 具体怎么选择?
由
JeandleUseHotspotIntrinsics控制。开启时:
关闭时:
伪代码是:
面试中最简洁的说法