Having fun with JNI: formatting a number

Posted on

Problem

I attempted some (easy) coding with Java Native Interface. This is what I have:

six_pack_Neatifier.h:

(autogenerated by javah)

#include <jni.h>

#ifndef INCLUDED_SIX_PACK_NEATIFIER
#define INCLUDED_SIX_PACK_NEATIFIER

#ifdef __cplusplus
extern "C" {
#endif

/*
 * Class:     six_pack_Neatifier
 * Method:    neatify
 * Signature: (JCI)Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_six_pack_Neatifier_neatify(JNIEnv*, 
                                                          jclass, 
                                                          jlong, 
                                                          jchar, 
                                                          jint);

#ifdef __cplusplus
}
#endif

#endif // INCLUDED_SIX_PACK_NEATIFIER

six_pack_Neatifier.cpp:

#include <sstream>

#include "six_pack_Neatifier.h"

JNIEXPORT jstring JNICALL Java_six_pack_Neatifier_neatify(JNIEnv* env, 
                                                          jclass clazz, 
                                                          jlong val, 
                                                          jchar pad, 
                                                          jint span)
{
    // Convert 'val' to a string.
    std::string number_string;
    std::stringstream strstream;
    strstream << val;
    strstream >> number_string;

    const char* raw = number_string.c_str();
    const int signlen = val < 0;
    const size_t digitlen = number_string.length() - signlen;
    // +1 for the C string null terminator.
    const size_t outlen = signlen + digitlen + (digitlen - 1) / span + 1;
    char *const out = new char[outlen];

    // Terminate the C string.
    out[outlen - 1] = '';

    int pos = outlen - 2;
    int src = number_string.size() - 1;

    const size_t ospan = span + 1;

    while (pos >= signlen) 
    {
        out[pos] = (outlen - 1 - pos) % ospan == 0 ?
                                               pad :
                                               raw[src--];
        --pos;
    }

    if (val < 0) 
    {
        out[0] = '-';
    }

    jstring ret = env->NewStringUTF(out);
    delete[] out;
    return ret;
}

Makefile (MacOSX):

leabnit.jnilib: six_pack_Neatifier.o
    g++ -dynamiclib -o libneat.jnilib six_pack_Neatifier.o

six_pack_Neatifier.o: six_pack_Neatifier.cpp
    g++ -std=c++11 -O3 -I/Developer/SDKs/MacOSX10.6.sdk/System/Library/Frameworks/JavaVM.framework/Versions/A/Headers/ -c six_pack_Neatifier.cpp 

Neatifier.java:

package six.pack;

import java.io.File;
import java.util.Scanner;

/**
 * This class implements a couple of digit grouping routines.
 * 
 * @author Rodion "rodde" Efremov
 * @version 1.6
 */
public class Neatifier {

    /**
     * Try load the native library.
     */
    static {
        try {
            System.load(System.getProperty("user.dir") + File.separator +
                        "src" + File.separator + "libneat.jnilib");
        } catch (final UnsatisfiedLinkError ule) {
            System.err.println("Could not load the native library. " + ule);
            System.exit(-1);
        } 
    }

    /**
     * Returns neat string representation of <code>val</code> using 
     * <code>pad</code> as the padding character, and groups of length 
     * <code>span</code>. Uses Rolfl's algorithm implemented in C++.
     * 
     * @param  val  the number to print neatly.
     * @param  pad  the padding character.
     * @param  span the length of a digit group.
     * @return a neat string.
     */
    public static native String neatify(final long val,
                                        final char pad,
                                        final int span);
    /**
     * The entry point into a program.

     * @param args the command line arguments.
     */
    public static void main(final String... args) {
        final Scanner scanner = new Scanner(System.in);

        while (scanner.hasNextLong()) {
            final long l = scanner.nextLong();

            System.out.println(neatify(l, '_', 3));
        }    
    }
}

Tell me anything that comes to mind.

Solution

JNI, cool, but why? Are you expecting C++ to be faster?

In an independent implementation, you may be right, a raw C++ implementation that has no JNI component may well be faster, but, the overheads in the interaction between Java and C++ are expensive. Each time there is a call between the systems, there needs to be a translation of all data passed, and returned. If there is a lot of work to be done on the C++ side, then the overhead is quickly amortized, and becomes “worth it”. If the C++ side is fast, though, then the bulk of the time is spent “in translation”.

In your case, as I suspected, the overhead far exceeds the actual time-to-format the numbers.

Review

So, about the review:

  • The header-file is auto-generated, and is not really your code.
  • In the implementation, you do … horrible things, like you convert the inptut to a C++ string, but then convert it again back to a C char*. The rest of the implementation is about what I would like to see (hey, I recognize that code…. 😉
  • The Java side looks only OK. I don’t like the absolute path for the library load… you should use the loadLibrary(...) call instead and ensure your library is on the library load path.
  • You don’t print the exception on a library load error, just the toString(). Losing exception data (and a possible cause) like that is… silly. Log the exception, or do a ex.printStackTrace();

Performance

I compared your JNI version against other versions from previous questions. To do this, I pulled the code on to a linux machine. There are two interesting things here….

  1. the version of code I recommended in my previous answer is still faster than your code, now on linux too…
  2. the JNI is slow in comparison.

Here’s the commandline I used (note, I removed the package declaration…):

g++ -std=c++11 -O3 -I${JAVA_HOME}/include -I${JAVA_HOME}/include/linux -shared -fPIC -o libneat.so Neatifier.cpp

Then, I added it to my UBench code, as:

public static String neatifyJNI(final long val) {
    return neatifyJNI(val, ' ', 3);
}

public static String neatifyJNI(final long val, final char pad, final int span) {
    return Neatifier.neatify(val, pad, span);
}

And my performance results are:

Task NumberPad -> OP: (Unit: MILLISECONDS)
  Count    :    10000      Average  :   0.1633
  Fastest  :   0.1376      Slowest  :   2.7011
  95Pctile :   0.2449      99Pctile :   0.3013
  TimeBlock : 0.190 0.155 0.152 0.156 0.156 0.147 0.158 0.219 0.149 0.150
  Histogram :  9810   146    33     9     2

Task NumberPad -> RL: (Unit: MILLISECONDS)
  Count    :    10000      Average  :   0.2480
  Fastest  :   0.2251      Slowest  :   3.0615
  95Pctile :   0.2732      99Pctile :   0.7174
  TimeBlock : 0.314 0.236 0.237 0.241 0.239 0.241 0.235 0.241 0.241 0.254
  Histogram :  9796   189    13     2

Task NumberPad -> RLP: (Unit: MILLISECONDS)
  Count    :    10000      Average  :   0.1228
  Fastest  :   0.1163      Slowest  :   2.7432
  95Pctile :   0.1398      99Pctile :   0.1720
  TimeBlock : 0.130 0.119 0.119 0.120 0.122 0.124 0.124 0.123 0.123 0.124
  Histogram :  9972    20     4     3     1

Task NumberPad -> JNI: (Unit: MILLISECONDS)
  Count    :    10000      Average  :   1.0328
  Fastest  :   0.9734      Slowest  :   5.3716
  95Pctile :   1.0931      99Pctile :   1.1412
  TimeBlock : 1.054 1.046 1.045 1.063 1.037 1.019 1.008 1.026 1.009 1.021
  Histogram :  9997     2     1

in essence, it is … 5 times slower than other options.

Here are some general disadvantages for JNI:

  1. Overhead of translation
  2. not able to inline the code by the JIT compiler
  3. portability

Leave a Reply

Your email address will not be published. Required fields are marked *