Boosting Android SDK performance using JNI

Posted on 28. Jan, 2011 by in Advanced

There’s a few reasons when to use JNI in a Java program

  • Wrap lower level functionality – Classes in the Android Application Framework uses JNI to interface with the underlying platform and hardware like Camera, Sensors and GPS.
  • Interface native legacy code – You might have some old legacy code written in C/C++ and you don’t want to waste your time porting the code to Java. With JNI you can create an interface class in Java that exposes the functionality of your legacy code.
  • Bypass performance bottlenecks – Execute heavy number crunching in native code and get rid of the overhead that the instruction interpretation in the JVM introduces.
  • On Android, in order to prevent fragmentation, we are only allowed to use the following libraries in our native code.

  • libc (C library) headers
  • libm (math library) headers
  • JNI interface headers
  • libz (Zlib compression) headers
  • liblog (Android logging) header
  • OpenGL ES 1.1 (3D graphics library) headers (since 1.6)
  • A Minimal set of headers for C++ support
  • In this blog I will demonstrate how to transform a time consuming Java method with a lot of number crunching into a native declared method where the real work is performed in native code. Here’s the time consuming Java method

    public double compare(int[] sourceData,int[] targetData, double targetError) {
      double error = 0.0D;
      for (int index = 0; index < targetData.length; index++) {
        int c1 = sourceData[index];
        int c2 = targetData[index];
        int b = (c1 >> 16 & 255) - (c2 >> 16 & 255);
        int g = (c1 >> 8 & 255) - (c2 >> 8 & 255);
        int r = (c1 & 255) - (c2 & 255);
        error += r * r + g * g + b * b;
        if (error > targetError)
          return error;
      }
      return error;
    }

    The sourceData and targetData arguments represents the pixels of two Bitmaps. In short the method calculates the sum of the square distance in color between two images, pixel by pixel. If we compare two 200×200 pixels images the for-loop will run 40000 times! This is a typical candidate for when to use JNI.

    What to do in Native

    You can use the boilerplate.c file as boilerplate code for all your native c functions.

    Implement the native function
    This is what the function will look like when written in C.

    static jdouble compareNative(JNIEnv *env, jobject thiz, jintArray sourceArr,
                                                    jintArray targetArr, jdouble targetError){
      jdouble error = 0.0;
      int index, c1, c2, b, g, r  = 0;
      jint *sarr, *tarr;
    
      sarr = (*env)->GetIntArrayElements(env, sourceArr, NULL);
      tarr = (*env)->GetIntArrayElements(env, targetArr, NULL);
    
      if (sarr == NULL || tarr == NULL) {
             return targetError; /* exception occurred */
      }
    
      int size = (*env)->GetArrayLength(env, sourceArr);
    
      for (index = 0; index < size; index++) {
        c1 = sarr[index];
        c2 = tarr[index];
        b = (c1 >> 16 & 255) - (c2 >> 16 & 255);
        g = (c1 >> 8 & 255) - (c2 >> 8 & 255);
        r = (c1 & 255) - (c2 & 255);
        error += r * r + g * g + b * b;
        if (error > targetError){
          (*env)->ReleaseIntArrayElements(env, sourceArr, sarr, 0);
          (*env)->ReleaseIntArrayElements(env, targetArr, tarr, 0);
          return error;
        }
      }
     (*env)->ReleaseIntArrayElements(env, sourceArr, sarr, 0);
     (*env)->ReleaseIntArrayElements(env, targetArr, tarr, 0);
     return error;
    }

    All native functions must have the JNIEnv (a reference to the virtual machine itself) and the jobject (a reference to the “this pointer” of the Java object where the native method call comes from) as the first two arguments. Then we can add our own arguments. For more information on how to write native code for JNI see the JNI Reference Documentation.

    Register your native functions
    We need a way to make the virtual machine direct the calls to the native declared Java method to our native C function. This is done using the registerNatives function of the JNIEnv. If you use the boilerplate C code from above you only have to do two things.

      Set the classpath variable to the full class name of your Java class (including package name). Replace the dots with slashes.

      For each native declared method in Java, insert a JNINativeMethod struct into the methods[] array.

    For our example it will look like this

    static const char *classPathName = "com/jayway/MyComparator";
    
    static JNINativeMethod methods[] = {
      // nameOfNativeMethod, methodSignature, methodPointer
      {"compare", "([I[ID)D", (void*)compareNative },
    };

    The first parameter is the name of the native declared Java method, the second is the signature of the native declared Java method and the last parameter is the function pointer to the C function to execute when the native declared Java function is executed.

    The signature of a Java method can be determined using the javap tool from SUN's Java SDK or you can create it yourself using the following table, Java VM Type Signatures.

    Build using Android NDK
    To make things really simple when developing JNI code Google has released the Android Native Development Kit (NDK). It's easy to setup and use. Just setup a new project according to the NDK documentation and build your project. In short, you create a folder named jni in your Android Eclipse project. Here you put all the c-files together with an Android.mk file. In the Android.mk you specify which c-files to compile. In the Android NDK/apps folder you create a directory named after your project (perhaps my-app). In this directory you add an Application.mk file. In the Application.mk, set the APP_PROJECT_PATH to the path of your Eclipse project. To create the lib simply type make APP=my-app from the console in your Android NDK folder. If everything goes well you will see a libs folder in your Eclipse project.

    What to do in Java

    In order to transform our Java method into a native declared method simply add the word native in the method signature and remove the implementation of the method.

    public native double compare(int[] sourceData, int[] targetData,
    			double targetError);

    To make sure that the virtual machine loads our library into memory and register our native functions we have to add the following code.

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

    This tells the virtual machine to load the library libcomparejni.so from the underlying operating system. The OnLoad function is executed on the library and our native functions gets registered with the virtual machine.

    The Result

    After some simple benchmarking I found that the native declared method executed about 2-3 times faster than the original method executing within Dalvik. For larger images the improvement might be even bigger since a call to a native declared method takes more time than calling a normal Java method. Eventually, when Dalvik supports JIT compilation, we would probably get the above performance boost for free without calling native functions. But bear in mind, most devices on the market today will never get an upgraded Dalvik with JIT.

    Tags: , ,

    Leave a Reply