0

I benchmarked a code that allocates 1 million java objects with

  1. env->NewObject
  2. env->AllocObject, env->SetLongField

To my surprise, the code took 250ms and 125ms to execute respectively. This is 10-5x slower than what an equivalent java program would take!

The overhead of the JNI call is also order of magnitude larger than the one mentioned in What makes JNI calls slow? (which goes in the opposite direction java -> Native).

What makes it so slow? Doesn't C have direct access to Java heap?

Alternatively, is there a more efficient way to pass data from C to java trough JNI?

The java code:

public class Main { 
   static {
      System.loadLibrary("main"); 
   }
 
   private native void bench();

   static void bench_java() {
      long now = System.currentTimeMillis();
      for (int i=0; i < 1000000; i++) new Number(i);
      System.out.println(System.currentTimeMillis() - now); 
   }

   public static void main(String[] args) {
      new Main().bench(); 
   }
}

public class Number {
   long j;

   public Number(long j) {
      this.j = j;
   }
}

The c code:

#include "jni.h"  
#include <stdio.h>   
#include <sys/time.h>

 

JNIEXPORT void JNICALL Java_Main_bench(JNIEnv *env, jobject thisObj) {
   struct timespec start, end;

   jclass    cls  = (*env)->FindClass(env, "Number");
   jmethodID init = (*env)->GetMethodID(env, cls, "<init>", "(J)V");

   clock_gettime(CLOCK_MONOTONIC, &start);
   for (int i=0; i<1000000; i++) (*env)->NewObject(env, cls, init, 0);
   clock_gettime(CLOCK_MONOTONIC, &end);

   printf("Time elapsed %lu ms\n", (end.tv_sec - start.tv_sec) * 1000 + (end.tv_nsec - start.tv_nsec) / 1000000);
   return;
}
Ford O.
  • 1,394
  • 8
  • 25
  • 1
    I haven't profiled this but there are a couple things that come to mind. Function call overhead in c, local ref bookkeeping overhead, no way to jit to improve efficiency /inline constructors. – PiRocks Sep 16 '20 at 07:08
  • I think we can rule out ref bookkeeping overhead since adding `DeleteLocalRef` didn't improve the performance. – Ford O. Sep 16 '20 at 07:42
  • 1
    I don't see how that rules out any bookkeeping overhead. The local references still have to be created. Adding `DeleteLocalRef` calls only ensures that you don't overflow the local reference table. – Michael Sep 16 '20 at 08:37
  • A thing to keep in mind that JNI has a lot of indirections. Like when you call `NewObject()`, that is a JNI-specific call, going to [`jni_NewObject`](https://hg.openjdk.java.net/jdk/jdk15/file/fb7064dc63f9/src/hotspot/share/prims/jni.cpp#l1104) presumably doing some extra check already, and then going to [`jni_invoke_nonstatic()`](https://hg.openjdk.java.net/jdk/jdk15/file/fb7064dc63f9/src/hotspot/share/prims/jni.cpp#l984) for invoking the constructor, which is still JNI-specific and rather generic, so it may be far more complicated with more checks than what the JVM would do itself. – tevemadar Sep 16 '20 at 10:16
  • 1
    How do you know `for (int i=0; i < 1000000; i++) new Number(i);` wasn't entirely optimized away? – Andrew Henle Sep 17 '20 at 09:14

0 Answers0