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 Cchar*
. 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 aex.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….
- the version of code I recommended in my previous answer is still faster than your code, now on linux too…
- 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:
- Overhead of translation
- not able to inline the code by the JIT compiler
- portability