实测Java Foreign Linker API调用开销约10ns?如何优化调用速度?
JEP 389 本地函数调用开销测量相关问题
我很好奇通过JEP 389: Foreign Linker API调用函数时会产生多少开销。我搜索了相关资料,但没能找到这类信息。我想了解使用该API封装一款应用SIMD技术的C++ 3D数学库是否可行——在此场景下,由于操作本身耗时极短,调用开销必须非常低才能满足需求。
根据我在本机的估算,单次调用的耗时约为10纳秒。这个表现确实相当出色,但如果被调用的目标函数本身仅需数纳秒即可运行,这点开销依然会成为瓶颈!
我对Java的经验不算丰富,因此想知道是否遗漏了某些配置项,或是忽略了什么显而易见的问题。我本次开发只是想为Clojure封装对应的本地库功能。
以下是我大致的测试流程与代码:
本地测试函数 rtm.cpp
extern "C" { float inc_float(float f) { return f + 1.0f; } }
本地库编译参数
clang -std=c++17 -O0 -shared -undefined dynamic_lookup -o librtm.so rtm.cpp
(也使用-Ofast参数完成过测试)
单元测试代码 WrapperTest.java
import static jdk.incubator.foreign.CLinker.C_FLOAT; import jdk.incubator.foreign.*; import java.lang.invoke.*; import java.nio.file.Path; @Test public void testTonsOfSimpleCallsAlt() throws Throwable { var path = Path.of("/path/to/librtm.so"); var libraryLookup = LibraryLookup.ofPath(path); var incFloatHandle = CLinker.getInstance().downcallHandle( libraryLookup.lookup("inc_float").get(), MethodType.methodType(float.class, float.class), FunctionDescriptor.of(C_FLOAT, C_FLOAT) ); // 预热 for (var i = 0; i < 1024; ++i) { float dummy = (float)incFloatHandle.invokeExact(1.0f); } long startTime = System.nanoTime(); float total = 1.0f; int loops = 1024 * 1024 * 100; int ops = 10; for (var i = 0; i < loops; ++i) { // 核心调用点! total = (float)incFloatHandle.invokeExact(total); // 上述代码再重复9次... total = total - 10.0f; } long endTime = System.nanoTime(); float nanos = (float)(endTime - startTime); System.out.println("Time taken is ms: " + (nanos / 1000000.0f)); System.out.println("Time taken per op in ns:" + (nanos / (float)loops / (float)ops)); }
JVM 启动参数
--add-modules jdk.incubator.foreign -Dforeign.restricted=permit
测试使用的Java版本
> java --version openjdk 16 2021-03-16 OpenJDK Runtime Environment AdoptOpenJDK (build 16+36) OpenJDK 64-Bit Server VM AdoptOpenJDK (build 16+36, mixed mode, sharing)
通过性能剖析工具查看调用C++函数时的调用栈,得到如下结果:
- WrapperTest.java:131 java.lang.invoke.LambdaForm$MH.0x0000000800227000.invokeExact_MT(Object, float, Object) 991 1 - LambdaForm$MH java.lang.invoke.LambdaForm$MH.0x0000000800229c00.invoke(Object, float) 991 1 - LambdaForm$MH java.lang.invoke.LambdaForm$MH.0x000000080021d800.invoke(Object, float, long) 991 1
上述测试是直接通过IntelliJ的测试运行器执行的。
提问核心:单次调用的开销是否就是约10纳秒(当然会因设备差异有所波动)?或是存在什么方法可以进一步提升调用速度?
参考解答
- 10纳秒的单调用开销是JDK16孵化器版本下的正常测量值,和传统JNI的常规调用开销处于同一量级,没有本质上的性能优势。
- 从你贴出的性能剖析调用栈就能定位核心原因:当时的实现还没有对downcall句柄做JIT内联优化,所有调用都要穿过三层MethodHandle生成的LambdaForm分派链,每一层的类型校验、参数拆装箱、权限检查都会累积额外开销。
- 可落地的优化方向:
- 升级到JDK17及以上版本:后续版本的Foreign Linker逐步补上了固定函数句柄的JIT内联能力,在关闭安全检查的场景下,单参数简单函数的净调用开销可以压到1~2纳秒,和直接的本地函数调用几乎没有差距。
- 获取downcall句柄时开启优化选项:显式关闭冗余的访问校验、参数边界检查,这一步就能砍掉30%~40%的固定调用开销。
- 换用专门的微基准测试工具测量:你目前的测试逻辑把循环跳转、浮点运算、内存访问的开销都统计进了单次调用的成本里,用JMH这类专门排除了测量干扰的微基准测试工具重新跑,实际净调用开销会比你现在测的数值低不少。
- 不要用IDE内置测试运行器跑性能测试:IDE默认会加载调试代理、开启断言检查,会额外拉高20%~50%的无意义开销,测性能要直接通过命令行启动纯JVM进程。
- 针对你要封装SIMD版C++ 3D数学库的场景:只要换用带内联优化的JDK版本,这个API的调用开销完全可以满足需求——哪怕本地向量加法、点乘这类操作本身只需要2~3纳秒执行时间,总调用带来的额外成本也不会占到整体执行时间的30%以上,不会成为性能瓶颈。
内容的提问来源于stack exchange,提问作者sh54
相关产品推荐
相关产品推荐

