JNI调用的定量开销是什么?

73

仅基于性能,大约需要多少行“简单”的Java代码才能与进行JNI调用的性能损失相等?

或者更具体地表达这个问题,如果执行一个简单的Java操作如下:

someIntVar1 = someIntVar2 + someIntVar3;

如果给定一个“CPU工作指数”1,那么进行JNI调用的开销(overhead)通常是多少(大致范围)?


这个问题忽略了等待本地代码执行所需的时间。用电话术语来说,它严格只涉及“呼叫费率”的“标志下降”部分,而不是“呼叫费率”的其他部分。


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

编辑:

有些人卡在CPU、RAM等方面的差异上。这些几乎与问题无关-我询问的是相对于Java代码的成本。如果CPU和RAM较差,则Java和JNI都会受到影响,因此环境考虑应该平衡。JVM版本也属于“无关紧要”的类别。

这个问题不是要求纳秒级别的绝对时间,而是以“简单Java代码行”的“工作量”单位来估计大致开销。


5
你看过什么导致JNI调用变慢了?吗? - Aviram Segal
2
@AviramSegal 是的,但是那里没有提到它的成本有多少,只是为什么它会有这个成本。 - Bohemian
2
我认为问题应该涉及“哪些因素导致了开销并且有多大影响”,因为我怀疑任何JNI调用都没有唯一的答案。 - Marko Topolnik
1
你正在调查哪个JVM?不同实现之间的差异巨大,而且时间差异也取决于CPU和RAM的选择。 - Alex Cohn
2
@Bohemian: 你的假设可能不成立。首先,JNI调用始终是一次调用;内联Java代码不涉及“函数调用开销”,这取决于CPU体系结构(32位模式下的x86 vs. 64位模式下的x86 vs. ARM等)。其次,内存高速缓存未命中(或匹配)的问题非常重要。最后,你不应该期望Sun/Oracle Java与Android(Dalvik)以相同的方式运作。 - Alex Cohn
显示剩余6条评论
3个回答

53

快速分析测试的结果为:

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;
}

结果:

单次调用 循环中的10次调用 循环中的100次调用

系统详情:

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

更新: Caliper 微基准测试适用于 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 ========

简单概括一下,似乎 JNI 调用在典型的 (x86) 硬件和 Hotspot VM 上大约相当于 10-25 次 java 操作。毫不意外,在优化程度远低于 Zero VM 的情况下,结果会有很大差异(3-4次操作)。


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


2
8.5 包括 test 和 testNative:/ 此外,您永远不想给出这样的性能比较结果。首先,您永远不应该使用分析器比较 A 比 B 更快的性能,您需要在发布模式下编译和进行微基准测试。其次,如果没有平均值和考虑离散度,那么数字就毫无意义,例如 8.5,但变异性为 6.8,则您的平均经过时间假设是错误的。 - SkyWalker
2
你已经接近回答这个问题了。试试这个方法:1)确保JIT已经编译了测试代码。2)不断向Java版本添加简单的算术行,直到两个时间相等,然后发布需要多少代码才能使两个调用“成本”相同。这就是我要找的答案。 - Bohemian
3
@GiovanniAzua:我不认为这是最终答案,而只是一个热身 :) 感谢您的评论(我非常感激),这变得很有趣 :) 我不会考虑这个答案是最终版本,而只是一个预热。感谢您的评论(我非常感激),现在变得更加有趣了 :) - barti_ddu
1
@barti_ddu,你不想在这里涉及太多的内存,因为那样会导致缓存未命中(这是一个巨大的差异)。我建议迭代地添加一个相对较大的质数int,从随机生成的初始值开始,并以某种方式使用该值(通常从测试方法中返回它)。这不能被优化掉,只使用堆栈。 - Marko Topolnik
@barti_ddu 好吧,那就赶快点,末日将至! - Marko Topolnik
显示剩余8条评论

9
所以,我刚在Windows 8.1(64位)使用Eclipse Mars IDE、JDK 1.8.0_74和VirtualVM Profiler 1.3.8与Profile Startup插件测试了对C的JNI调用的“延迟”。
设置:(两种方法) SOMETHING()传递参数,执行操作并返回参数 NOTHING()传递相同的参数,不进行任何操作,并返回相同的参数。
(每个方法被调用270次) SOMETHING()的总运行时间为:6523毫秒 NOTHING()的总运行时间为:0.102毫秒 因此,在我的情况下,JNI调用是相当可以忽略不计的。

尽管这并不完全是我所问的,但这仍然是一个有趣和相关的发现。 - Bohemian
啊,是的;我正在阅读Azua关于“延迟”的回答,结果决定测试一下 :) - VeraKozya
1
我同意你所写的内容,但0.1毫秒相当于每秒10,000次调用,或者20百万个周期。这是非常巨大的。 - Daniel Lemire
2
0.1毫秒是270次调用的总时间,每个NOTHING()函数调用需要0.4微秒。也就是说,每秒可以执行2.7百万次调用。 - cubic lettuce

1
你应该自己测试一下“延迟”是多少。 在工程上,延迟指发送长度为零的消息所需的时间。在这个背景下,它将对应于编写调用do_nothing空C ++函数的最小Java程序,并计算30个测量值(进行几个额外的预热调用)的经过时间的平均值和标准偏差。您可能会惊讶于使用不同JDK版本和平台执行相同操作的不同平均结果。
只有这样做才能给出最终答案,即是否在目标环境中使用JNI有意义。

2
我基本上是在问是否有人做过这个,他们能分享一下他们发现了什么:/ - Bohemian
3
这段话的意思是:这些数字没有任何意义,我预计由于底层平台和JDK版本的差异会有很大的不同。 - SkyWalker
3
计算机的差异(例如CPU和RAM)对于这个问题基本上没有影响。我是想知道以“Java代码行数”为代价的成本。这样就可以排除任何计算机问题——如果Java运行缓慢,JNI也会很慢等等——这就是我提出问题的原因。由于同样的原因,这应该也可以排除JVM问题。 - Bohemian
1
@Bohemian:我认为,如果您在回答中包含mbench代码,那将是公平的;无论如何,谢谢。 - barti_ddu
@GiovanniAzua :之前的评论实际上是针对您的,抱歉搞错了收件人。 - barti_ddu

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接