JDK外部函数Panama API性能超过了JNI


Java 本地调用 API 的 JMH 性能基准:JNI(通过JavaCpp)、JNAJNRBridjJDK JEP-424外部函数/内存 API(预览版)。

结论:Java的外部函数接口现在在一个简单的测试中优于JNI。

测试:
GetSystemTime使用 kernel32.dll 提供的Windows API 函数本机调用从当前系统时间获取秒数:

void GetSystemTime(LPSYSTEMTIME lpSystemTime);
数据结构定义为

typedef struct _SYSTEMTIME {
  WORD wYear;
  WORD wMonth;
  WORD wDayOfWeek;
  WORD wDay;
  WORD wHour;
  WORD wMinute;
  WORD wSecond;
  WORD wMilliseconds;
} SYSTEMTIME, *PSYSTEMTIME, *LPSYSTEMTIME;

每次实施都会
  • 为SYSTEMTIME结构体分配内存
  • 调用本机方法GetSystemTime传递分配的内存
  • 从字段中提取并返回值wSecond

在一个单独的基准测试中,我仅测量了本机调用的性能(第 2 项)。

JNI
JNI 是 Java 调用 JDK 早期版本中存在的本机代码的标准方法。JNI 需要构建一个本机存根作为 Java 和本机库之间的适配器,因此被认为是低级的。开发辅助工具是为了自动化和简化本机存根生成。这里我使用了JavaCpp,该项目以围绕高性能 C/C++ 库(例如 OpenCV 和 ffmpeg)预烘焙 Java 包装器而闻名。JavaCpp 为广泛使用的系统库(包括 Windows API 库)提供了现成的包装器,因此我在此基准测试中使用了它们。

JNA
JNA 通过使用动态调用目标函数的本机存根来解决编写本机包装器的负担。它只需要编写 Java 代码并提供到 C 结构和联合的映射,但是,对于编写与本机库的 C API 相匹配的 Java API 的复杂库来说仍然可能是一项艰巨的任务。JNA 还为 Windows API 提供预制 Java 类。与 JNI 相比,动态包装调用会导致较高的性能开销。

JNA Direct
JNA 的直接模式声称“大幅提高性能,接近自定义 JNI”。应该可以清楚地看到,调用主要使用原始类型作为参数和返回值。

BriJ
Bridj 试图提供类似于 JNA 的 Java 到 Cpp 互操作解决方案(无需编写和编译本机代码),它声称使用 dyncall 和手动优化的程序集调整来提供更好的性能。名为 JNAerator 的工具有助于从本机库头生成 java 类。Bridj 项目现在似乎已被放弃。

JNR
JNR 是一个相对年轻的项目,旨在解决相同的问题。与 JNA 或 Bridj 类似,它不需要本机编程。目前还没有太多的文档或评论,但 JNR 通常被认为是有前途的。

Panama API/JDK 外部函数/内存 API 预览版 (JEP-424) API
Java 程序可以通过该 API 与 Java 运行时之外的代码和数据进行互操作。

纯Java
作为比较,用JDK实现了同样的问题java.util.Date,java.util.Calendar并且java.time.LocalDateTime

测试结果:
结果
系统:英特尔酷睿 i7-10610U CPU @ 1.80GHz / Windows 10 / openjdk-19.0.1

Full benchmark (average time, smaller is better)

JmhGetSystemTimeSeconds.jnaDirect            4517.766 ± 417.656  ns/op
JmhGetSystemTimeSeconds.jna                  4037.103 ± 681.270  ns/op
JmhGetSystemTimeSeconds.bridj                1087.531 ± 122.028  ns/op
JmhGetSystemTimeSeconds.jnr                   400.896 ±  52.783  ns/op
JmhGetSystemTimeSeconds.jni_javacpp           259.521 ±   7.964  ns/op
JmhGetSystemTimeSeconds.foreign               237.920 ±  30.081  ns/op
JmhGetSystemTimeSeconds.java_calendar         154.341 ±   8.306  ns/op
JmhGetSystemTimeSeconds.java_localdatetime     85.310 ±  32.671  ns/op
JmhGetSystemTimeSeconds.java_date              58.209 ±   3.257  ns/op

  • JNA 看起来很慢(比 JNI 慢 13 倍)。JNA direct 显得更慢,因为将结构从 C 映射到 Java 可能会消耗大部分操作时间。
  • JNR 看起来比过时的 Bridj 更快,但仍然落后于 JNI。
  • JDK 的外部 API 表现出的性能比 JNI 快两倍。这看起来比 2019 年的结果好得多,证实了重要的性能优化工具位于 JDK 15-19 中。
  • Panama API本身还是比纯Java慢一点。请注意,最快的 API 是java.util.Date(已弃用但仍在工作Date.getSeconds)。JDK8+ 的LocalDateTime速度比 Calendar API 快约 2.4 倍,但比旧式j.u.Date.

现在让我们仅研究本机调用的性能,去掉结构分配和字段访问:

Native call only (average time, smaller is better)

JmhCallOnly.jna_direct                       1373.435 ±  70.343  ns/op
JmhCallOnly.jna                              1346.036 ±  72.239  ns/op
JmhCallOnly.bridj                             383.992 ±  50.000  ns/op
JmhCallOnly.jnr                               298.334 ±  48.785  ns/op
JmhCallOnly.jni_javacpp                        56.605 ±   8.087  ns/op
JmhCallOnly.foreign                            49.717 ±   6.667  ns/op

顺序几乎相同,Panama 处于领先地位。

Panama 还可以进一步提高性能:

  • A) 使用池化 Arena 进行分配。请参阅此处的基准: - issuecomment-1658327470>https://github.com/openjdk/panama-foreign/pull/854issuecomment-1658327470
  • B)传递Linker.Option.critical()/isTrivial()给downcallHandle选项 - https://github.com/openjdk/panama-foreign/pull/859
    *临界函数是指在任何情况下运行时间都极短的函数*(类似于调用空函数),并且不会回调到 Java 中(例如使用向上调用存根)。