You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

实测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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.24 12:24:03