Wie hoch ist der quantitative Overhead bei einem JNI-Aufruf?

Lesezeit: 7 Minuten

Benutzer-Avatar
Bohemien

Wie viele “einfache” Java-Zeilen allein auf der Grundlage der Leistung sind ungefähr die äquivalenten Leistungseinbußen bei einem JNI-Aufruf?

Oder um die Frage konkreter auszudrücken, ob eine einfache Java-Operation wie z

someIntVar1 = someIntVar2 + someIntVar3;

erhielt einen “CPU work”-Index von 1was wäre der typische (normale) “CPU-Arbeits”-Index für den Overhead beim Tätigen des JNI-Aufrufs?

Diese Frage ignoriert die Wartezeit auf die Ausführung des nativen Codes. Im Telefonjargon geht es ausschließlich um den “Flag-Fall”-Teil des Anrufs, nicht um die “Anrufrate”.

Der Grund für diese Frage ist, eine “Faustregel” zu haben, um zu wissen, wann man sich die Mühe machen sollte, einen JNI-Aufruf zu codieren, wenn man die nativen Kosten (aus direkten Tests) und die Java-Kosten einer bestimmten Operation kennt. Es könnte Ihnen helfen, schnell den Aufwand zu vermeiden, den JNI-Aufruf zu codieren, nur um festzustellen, dass der Callout-Overhead alle Vorteile der Verwendung von nativem Code aufzehrt.

Bearbeiten:

Einige Leute hängen sich an Variationen in CPU, RAM usw. auf. Diese sind für die Frage praktisch irrelevant – ich frage nach dem relativ Kosten für Zeilen Java-Code. Wenn CPU und RAM schlecht sind, sind sie sowohl für Java als auch für JNI schlecht, sodass sich Umweltaspekte ausgleichen sollten. Die JVM-Version fällt ebenfalls in die Kategorie “irrelevant”.

Diese Frage fragt nicht nach einem absoluten Timing in Nanosekunden, sondern nach einem “Arbeitsaufwand” in Einheiten von “Zeilen einfachen Java-Codes”.

  • Haben Sie einen Blick auf Was macht JNI-Aufrufe langsam geworfen?

    – Aviram Segal

    20. Dezember 2012 um 13:18 Uhr


  • @AviramSegal Ja, aber es gibt nichts darüber, wie viel es kostet nur warum es kostet

    – Böhmisch

    20. Dezember 2012 um 13:21 Uhr

  • Ich denke, die Frage sollte beinhalten, “welche Faktoren zum Overhead beitragen und wie viel”, weil ich bezweifle, dass es für jeden JNI-Aufruf eine eindeutige Antwort gibt.

    – Marko Topolnik

    20. Dezember 2012 um 13:25 Uhr

  • Welche JVM untersuchen Sie? Die Unterschiede zwischen den Implementierungen sind enorm; Außerdem unterscheidet sich das Timing je nach CPU- und RAM-Auswahl erheblich.

    – Alex Cohn

    20. Dezember 2012 um 15:55 Uhr

  • @Bohemian: Ihre Annahmen gelten möglicherweise nicht. Zunächst einmal ist ein JNI-Aufruf immer ein Anruf; Inline-Java-Code beinhaltet keinen “Funktionsaufruf-Overhead”, der von der CPU-Architektur abhängt (x86 im 32-Bit-Modus vs. x86 im 64-Bit-Modus vs. ARM und mehr). Zweitens ist die Frage nach Speicher-Cache-Fehlschlägen (oder Übereinstimmungen) sehr wichtig. Schließlich erwarten Sie nicht, dass Sun/Oracle Java genauso funktioniert wie Android (Dalvik).

    – Alex Cohn

    21. Dezember 2012 um 14:34 Uhr

Benutzer-Avatar
barti_ddu

Quick-Profiler-Testergebnisse:

Java-Klasse:

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-Bibliothek:

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

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

Ergebnisse:

einmaliger Aufruf
10 Anrufe in einer Schleife
100 Anrufe in einer Schleife

Systemdetails:

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

Aktualisieren: Caliper Mikro-Benchmarks für x86 (32/64 Bit) und ARMv6 sind wie folgt:

Java-Klasse:

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

Ergebnisse (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 ==

Ergebnisse (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 =

Ergebnisse (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 ========

Um die Dinge ein wenig zusammenzufassen, es scheint so JNI Aufruf entspricht ungefähr 10-25 Java-Operationen bei einem typischen (x86) Hardware und Hotspot-VM. Kein Wunder, unter viel weniger optimiert Null-VMdie Ergebnisse sind ziemlich unterschiedlich (3-4 ops).


Danke an @Giovanni Azua und @Marko Topolnik für die Teilnahme und Hinweise.

  • 8.5 enthält sowohl test als auch testNative :/ Außerdem möchten Sie niemals solche Leistungsvergleichsergebnisse liefern. Zuerst vergleichen Sie niemals die Leistung von A ist schneller als B mit einem Profiler, Sie müssen im Release-Modus und Mikrobenchmark kompilieren. Zweitens bedeutet die Zahl ohne Mittelung und Berücksichtigung der Streuung nichts, z. B. 8,5, aber die Variabilität beträgt 6,8, dann ist Ihre Annahme der mittleren verstrichenen Zeit BS.

    – Skywalker

    20. Dezember 2012 um 19:25 Uhr


  • Sie sind kurz davor, diese Frage zu beantworten. Versuchen Sie Folgendes: 1) Stellen Sie sicher, dass JIT den Testcode kompiliert hat. 2) Fügen Sie der Java-Version weiterhin einfache Zeilen hinzu, die einfache Arithmetik ausführen, bis die beiden Timings gleich sind, und posten Sie dann, wie viel Code erforderlich ist, damit die beiden Aufrufe dasselbe “kosten”. Das ist die Antwort, die ich suche

    – Böhmisch

    20. Dezember 2012 um 20:05 Uhr

  • @GiovanniAzua: Ich würde dies nicht als endgültige Antwort betrachten, eher als Aufwärmen 🙂 Danke für den Kommentar (ich weiß das wirklich zu schätzen), es wird interessant 🙂

    – barti_ddu

    20. Dezember 2012 um 20:34 Uhr

  • @Bohemian: Zählt die int-Addition aus vorgenerierten Zufallssätzen als einfache Arithmetik?

    – barti_ddu

    20. Dezember 2012 um 20:35 Uhr

  • @barti_ddu Sie möchten nicht zu viel Speicher einbeziehen, da Sie dies dann durch Cache-Fehler verzerren (und das ist ein enormer Unterschied). Ich schlage vor, iterativ eine große Primzahl hinzuzufügen intausgehend von einem zufällig generierten Anfangswert, und verwenden diesen Wert irgendwie (gibt ihn normalerweise von der getesteten Methode zurück). Dies kann nicht wegoptimiert werden und verwendet nur den Stack.

    – Marko Topolnik

    21. Dezember 2012 um 6:49 Uhr


Benutzer-Avatar
VeraKozya

Also habe ich gerade die „Latenz“ für einen JNI-Aufruf an C unter Windows 8.1, 64-Bit, mit der Eclipse Mars IDE, JDK 1.8.0_74 und VirtualVM Profiler 1.3.8 mit dem Profile Startup Add-on getestet.

Setup: (zwei Methoden)
SOMETHING() übergibt Argumente, erledigt Sachen und gibt Argumente zurück
NOTHING() übergibt dieselben Argumente, macht nichts mit ihnen und gibt dieselben Argumente zurück.

(jeder wird 270 mal angerufen)
Gesamtlaufzeit für ETWAS(): 6523ms

Gesamtlaufzeit für NICHTS(): 0,102 ms

Daher sind in meinem Fall die JNI-Aufrufe ziemlich vernachlässigbar.

  • Obwohl dies nicht ganz das ist, wonach ich gefragt habe, ist es dennoch eine interessante und relevante Erkenntnis.

    – Böhmisch

    22. März 2016 um 1:07 Uhr

  • Ah ja; Ich habe Azuas Antwort zu “Latenz” gelesen und das stattdessen getestet 🙂

    – VeraKozya

    22. März 2016 um 23:44 Uhr

  • Ich stimme dem zu, was Sie geschrieben haben, aber 0,1 ms entsprechen 10.000 Aufrufen pro Sekunde oder 20 Millionen Zyklen. Das ist enorm.

    – Daniel Lemire

    27. Januar 2017 um 22:18 Uhr

  • 0,1 ms sind die Gesamtzeit für 270 Aufrufe, was 0,4 µs pro einzelnem Aufruf von NOTHING() ergibt. Das sind 2,7 Millionen Aufrufe pro Sekunde.

    – Würfelsalat

    25. August 2017 um 7:10 Uhr

Sie sollten eigentlich selbst testen, was die “Latenz” ist. Die Latenz wird in der Technik als die Zeit definiert, die zum Senden einer Nachricht der Länge Null benötigt wird. In diesem Zusammenhang würde es dem Schreiben des kleinsten Java-Programms entsprechen, das a aufruft do_nothing leere C++-Funktion und berechnen Sie Mittelwert und stddev der verstrichenen Zeit über 30 Messungen (machen Sie ein paar zusätzliche Aufwärmaufrufe). Sie werden vielleicht von den unterschiedlichen Durchschnittsergebnissen überrascht sein, die für verschiedene JDK-Versionen und Plattformen dasselbe tun.

Nur so erhalten Sie die endgültige Antwort, ob die Verwendung von JNI für Ihre Zielumgebung sinnvoll ist.

  • Ich frage im Grunde, ob jemand dies getan hat und teilen könnte, was er gefunden hat: /

    – Böhmisch

    20. Dezember 2012 um 13:27 Uhr

  • Es ist irrelevant, ich würde aufgrund der zugrunde liegenden Plattform und der JDK-Versionen große Unterschiede erwarten. Diese Zahl wird nichts bedeuten.

    – Skywalker

    20. Dezember 2012 um 13:28 Uhr


  • Maschinenunterschiede (z. B. CPU und RAM) sind für diese Frage praktisch irrelevant. Ich fragte nach den Kosten in Form von “Zeilen Java-Code”. Dadurch werden alle Maschinenprobleme aufgehoben – wenn Java langsam ist, wird JNI langsam sein usw. – weshalb ich die Frage so gestellt habe, wie ich es getan habe. Aus dem gleichen Grund sollte es auch JVM-Probleme beseitigen

    – Böhmisch

    20. Dezember 2012 um 19:59 Uhr


  • @Bohemian: Ich denke, es wäre fair, wenn Sie den MBench-Code in Ihre Antwort aufnehmen würden. trotzdem danke.

    – barti_ddu

    20. Dezember 2012 um 22:29 Uhr

  • @GiovanniAzua: Der vorherige Kommentar war dir eigentlich gewidmet; Entschuldigung für die falsche Adressierung.

    – barti_ddu

    21. Dezember 2012 um 8:35 Uhr

1141330cookie-checkWie hoch ist der quantitative Overhead bei einem JNI-Aufruf?

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy