进行 JNI 调用的定量开销是多少?

仅仅基于性能,大约有多少“简单”的 java 行相当于进行 JNI 调用的性能损失?

或者尝试以更具体的方式表达问题,如果一个简单的 java 操作,如

someIntVar1 = someIntVar2 + someIntVar3;

如果给定一个 1的“ CPU 工作”索引,那么使用 JNI 调用的开销的典型(大致)“ CPU 工作”索引是什么?


此问题忽略等待本机代码执行所花费的时间。在电话用语中,它是严格关于“降旗”部分的呼叫,而不是“呼叫频率”。


提出这个问题的原因是有一个“经验法则”,当您知道给定操作的本机成本(来自直接测试)和 Java 成本时,就可以知道何时尝试编写 JNI 调用代码。它可以帮助您快速地避免为 JNI 调用编写代码时发现调出开销消耗了使用本机代码的任何好处所带来的麻烦。

编辑:

有些人正在纠结于 CPU、 RAM 等的变化。这些实际上都与问题无关——我要求的是 Java 代码行的 亲戚成本。如果 CPU 和 RAM 很差,那么对于 java 和 JNI 来说,它们都很差,因此环境方面的考虑应该得到平衡。JVM 版本也属于“无关”类别。

这个问题并不是要求以纳秒为单位的绝对计时,而是要求以“简单 Java 代码行”为单位的“工作努力”。

22868 次浏览

实际上,您应该自己测试“延迟”是什么。在工程中,延迟被定义为发送零长度消息所需的时间。在这种情况下,它相当于编写最小的 Java 程序,调用一个 do_nothing空的 C + + 函数,计算30次测量所用时间的平均值和标准开发值(做几个额外的预热调用)。对于不同的 JDK 版本和平台,不同的平均结果会产生相同的结果,您可能会感到惊讶。

只有这样做才能给出使用 JNI 对于目标环境是否有意义的最终答案。

快速分析器测试结果:

Java 类:

public class Main {
private static native int zero();


private static int testNative() {
return Main.zero();
}


private static int test() {
return 0;
}


public static void main(String[] args) {
testNative();
test();
}


static {
System.loadLibrary("foo");
}
}

C 库:

#include <jni.h>
#include "Main.h"


JNIEXPORT int JNICALL
Java_Main_zero(JNIEnv *env, jobject obj)
{
return 0;
}

结果:

single invocation 10 calls in a loop 100 calls in a loop

系统详情:

java version "1.7.0_09"
OpenJDK Runtime Environment (IcedTea7 2.3.3) (7u9-2.3.3-1)
OpenJDK Server VM (build 23.2-b09, mixed mode)
Linux visor 3.2.0-4-686-pae #1 SMP Debian 3.2.32-1 i686 GNU/Linux

更新: X86(32/64位)和 ARMv6的卡尺微基准测试如下:

Java 类:

public class Main extends SimpleBenchmark {
private static native int zero();
private Random random;
private int[] primes;


public int timeJniCall(int reps) {
int r = 0;
for (int i = 0; i < reps; i++) r += Main.zero();
return r;
}


public int timeAddIntOperation(int reps) {
int p = primes[random.nextInt(1) + 54];   // >= 257
for (int i = 0; i < reps; i++) p += i;
return p;
}


public long timeAddLongOperation(int reps) {
long p = primes[random.nextInt(3) + 54];  // >= 257
long inc = primes[random.nextInt(3) + 4]; // >= 11
for (int i = 0; i < reps; i++) p += inc;
return p;
}


@Override
protected void setUp() throws Exception {
random = new Random();
primes = getPrimes(1000);
}


public static void main(String[] args) {
Runner.main(Main.class, args);
}


public static int[] getPrimes(int limit) {
// returns array of primes under $limit, off-topic here
}


static {
System.loadLibrary("foo");
}
}

结果(x86/i7500/Hotspot/Linux) :

Scenario{benchmark=JniCall} 11.34 ns; σ=0.02 ns @ 3 trials
Scenario{benchmark=AddIntOperation} 0.47 ns; σ=0.02 ns @ 10 trials
Scenario{benchmark=AddLongOperation} 0.92 ns; σ=0.02 ns @ 10 trials


benchmark     ns linear runtime
JniCall 11.335 ==============================
AddIntOperation  0.466 =
AddLongOperation  0.921 ==

结果(amd64/phenom 960T/Hostspot/Linux) :

Scenario{benchmark=JniCall} 6.66 ns; σ=0.22 ns @ 10 trials
Scenario{benchmark=AddIntOperation} 0.29 ns; σ=0.00 ns @ 3 trials
Scenario{benchmark=AddLongOperation} 0.26 ns; σ=0.00 ns @ 3 trials


benchmark    ns linear runtime
JniCall 6.657 ==============================
AddIntOperation 0.291 =
AddLongOperation 0.259 =

结果(armv6/BCM2708/Zero/Linux) :

Scenario{benchmark=JniCall} 678.59 ns; σ=1.44 ns @ 3 trials
Scenario{benchmark=AddIntOperation} 183.46 ns; σ=0.54 ns @ 3 trials
Scenario{benchmark=AddLongOperation} 199.36 ns; σ=0.65 ns @ 3 trials


benchmark  ns linear runtime
JniCall 679 ==============================
AddIntOperation 183 ========
AddLongOperation 199 ========

总结一下,在典型的(X86)硬件和 Hotspot VM上,JNI调用大致相当于10-25个 java 操作。毫不奇怪,在更少优化的 零虚拟机下,结果是完全不同的(3-4操作)。


感谢@Giovanni Azua和@Marko Topolnik的参与和提示。

因此,我刚刚测试了在 Windows 8.1,64位,使用 Eclipse Mars IDE,JDK 1.8.0 _ 74和带 Profile Startup 附加组件的 VirtualVM profiler 1.3.8对 C 的 JNI 调用的“延迟”。

设置: (两种方法)
SOMETHING ()传递参数、完成操作并返回参数
NOTHING ()传入相同的参数,不对它们进行任何操作,并返回相同的参数。

(每个人都被叫了270次)
SOMETHING ()的总运行时间: < strong > 6523ms

NOTHING ()的总运行时间: < strong > 0.102 ms

因此在我的例子中,JNI 调用是可以忽略不计的。