Java Tip 141: Fast math with JNI

http://www.javaworld.com/javaworld/javatips/jw-javatip141.html

While developing a computer-generated hologram (CGH) program, I noticed that the math routines in Java 2 Platform, Standard Edition (J2SE) 1.4x were several times slower than the corresponding routines in J2SE 1.3.1. Sun Microsystems has implemented a new math package, StrictMath, in J2SE 1.4 that guarantees identical math calculations across all platforms at the expense of execution speed. My CGH program ran seven times slower with the new J2SE 1.4x routines! I didn't want to switch back to J2SE 1.3.1, however, because I wanted to use the new javax.imageio package to save my holograms to disk as .png files. I submitted some bug reports to Sun about this serious performance regression, but received no meaningful response.

JNI to the rescue

Since my trusty C++ compiler produced fast math code, I just needed to call these math routines instead of the slow StrictMathroutines from my Java program. Java Native Interface (JNI) enables Java code to interface with native code written in other languages like C/C++. It enables you to write the bulk of your application in Java and still utilize highly optimized native code where necessary. And in the corporate computing world, JNI enables Java applications to connect to legacy code that might be difficult or impractical to port to Java.

To build my JNI math library, I first create a Java class, MathLib, which declares three native methods, cos()sin(), and sqrt(). I add a main method to test these native methods:

package com.softtechdesign.math;
/**
 * MathLib declares native math routines to get around the slow StrictMath math
 * routines in JDK 1.4x
 * @author Jeff S Smith
 */
public class MathLib
{
    public static native double sin(double radians);
    public static native double cos(double radians);
    public static native double sqrt(double value);
    static 
    {
        System.loadLibrary("MathLib");
    }
    
    public static void main(String[] args) 
    {
        System.out.println("MathLib benchmark..." + new java.util.Date());
        double d = 0.5;
        for (int i=0; i < 10000000; i++)
        {
            d = (d * i) / 3.1415926;
            d = MathLib.sin(d);
            d = MathLib.cos(d);
            d = MathLib.sqrt(d) * 3.1415926;
        }
        
        System.out.println("Benchmark done. Time is " + new java.util.Date());
    }
}


The static initializer, which executes before the code in the main method, loads the MathLib library. The main method performs a benchmark test of the routines by calling them in a loop. Next, I compile my class and create a header (.h) file for it by using the standard javah tool:

javah -jni -o MathLib.h -classpath . com.softtechdesign.math.MathLib


This command creates a file called MathLib.h that contains the following C function signatures: cos()sin(), and sqrt(). This C header file includes <jni.h>, the standard header file for JNI applications. Note how the fully qualified class name becomes part of the C function name. MathLib.h looks like this:

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_softtechdesign_math_MathLib */
#ifndef _Included_com_softtechdesign_math_MathLib
#define _Included_com_softtechdesign_math_MathLib
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     com_softtechdesign_math_MathLib
 * Method:    sin
 */
JNIEXPORT jdouble JNICALL Java_com_softtechdesign_math_MathLib_sin
  (JNIEnv *, jclass, jdouble);
/*
 * Class:     com_softtechdesign_math_MathLib
 * Method:    cos
 */
JNIEXPORT jdouble JNICALL Java_com_softtechdesign_math_MathLib_cos
  (JNIEnv *, jclass, jdouble);
/*
 * Class:     com_softtechdesign_math_MathLib
 * Method:    sqrt
 */
JNIEXPORT jdouble JNICALL Java_com_softtechdesign_math_MathLib_sqrt
  (JNIEnv *, jclass, jdouble);
#ifdef __cplusplus
}
#endif
#endif


Next, I create a file called Math.c and implement the native C code that includes the three math routines using the signatures found in my MathLib.h file. Note that these routines simply call the standard C versions of sin()cos(), and sqrt():

#include <jni.h>
#include <math.h>
#include "MathLib.h"
#include <stdio.h>
JNIEXPORT jdouble JNICALL Java_com_softtechdesign_math_MathLib_sin(JNIEnv *env, jobject obj, jdouble value)
{
    return(sin(value));
}
JNIEXPORT jdouble JNICALL Java_com_softtechdesign_math_MathLib_cos(JNIEnv *env, jobject obj, jdouble value)
{
    return(cos(value));
}
JNIEXPORT jdouble JNICALL Java_com_softtechdesign_math_MathLib_sqrt(JNIEnv *env, jobject obj, jdouble value)
{
    return(sqrt(value));
}


The routines all return a jdouble and have two standard JNI parameters, JNIEnv (JNI interface pointer) and jobject (a reference to the calling object itself, similar to this). All implemented JNI routines have these first two parameters (even if the native method doesn't declare any parameters). The third parameter is my user-defined parameter that contains the value I pass into the function. These routines calculate the sin()cos(), or sqrt() of this parameter and return the value as a jdouble.

A word about JNI types

Native methods can access the following types:

Boolean  jboolean
string   jstring
byte     jbyte
char     jchar
short    jshort
int      jint
long     jlong
float    jfloat
double   jdouble
void     void 


All of these types can be directly accessed except for jstring, which requires a subroutine call to convert a Java Unicode string (2 bytes) to a C-style char* string (1 byte UTF-8 format). To convert a jstring to a C-style string, you might write code like the following:

JNIEXPORT void JNICALL
Java_MyJavaClass_printName(JNIEnv *env, jobject obj, jstring name)
{
    const char *str = (*env)->GetStringUTFChars(env, name, 0);
    printf("%s", name);
      //Need to release this string when done with it
      //to avoid memory leak
    (*env)->ReleaseStringUTFChars(env, name, str);
}


You can use the (*env)->NewStringUTF() function to create a new jstring from a C-style string. For example, a C function can contain the following code:

JNIEXPORT jstring JNICALL
Java_MyJavaClass_getName(JNIEnv *env, jobject obj)
{
    return (*env)->NewStringUTF(env, "Fred Flintstone");
}


Back to finishing MathLib

Continuing with MathLib, I compile my C code into a library (a Dynamic Link Library (DLL) since my code runs under Windows). I use Borland C++ Builder to create my DLL. If you use C++ Builder, make sure you create a DLL project (named MathLib) and then add the Math.c file to it. You also need to add the following to your compiler's include path:

  \javadir\include
  \javadir\include\win32


C++ Builder creates a library named MathLib.dll when it builds this project.

If you compile a Windows C library using Visual C++, you compile it like so:

cl -Ic:\javadir\include -Ic:\javadir\include\win32 -LD Math.c -FeMathLib.dll


And if you compile a C library under Solaris, you use the following command:

cc -G -I/javadir/include -I/javadir/include/solaris Math.c -o MathLib.so


The final step entails adding my DLL directory to my Java runtime library path (alternatively, I can copy the DLL to my Java program's working directory). If the Java code can't find the library, you will get a java.lang.UnsatisfiedLinkError. Try running your Java program:

java -classpath . com.softtechdesign.math.MathLib


The benchmark code in your main method should execute. If you want to compare the execution speed with the StrictMath versions, change the calls in MathLib.java from MathLib.xxx() to Math.xxx(). On my Pentium 4, 2-GHz machine, the benchmark took 24.5 seconds with the StrictMath routines and 7 seconds with the MathLib (JNI) routines. Not a bad performance boost!

The MathLib (JNI) routines are about half as fast as the Math routines in J2SE 1.3.1—a result of the overhead involved in making a JNI call. Note that when you make numerous calls to the MathLib routines in a tight loop, you sometimes get an exception:

Unexpected Signal : EXCEPTION_FLT_STACK_CHECK occurred at PC=0x9ED0D2


I submitted a bug report to Sun about this problem, but I have not received a response. The good news is that in practice, I have never gotten the error when running any of my hologram program code.

Hooray for JNI

JNI enables programmers to employ fast C/C++ math routines in their Java programs. It solves problems for which Java is poorly suited: when you need the speed of C or assembler, or when you need to write low-level code to communicate directly with hardware. It can also be used to access legacy applications or interface with libraries written in other languages.

This MathLib example barely scratches the surface of what you can do with JNI. JNI native methods can create and manipulate Java objects such as strings and arrays and accept Java objects as parameters. JNI can even catch and throw exceptions from native methods and handle these exceptions from the native code or from your Java application. This almost seamless sharing of objects makes it very easy to incorporate native methods in your Java applications.

About the author

Jeff S. Smith is president of SoftTech Design, a Web and software development company located near Denver, Colo. Jeff has a BA in physics from the University of Virginia and 15 years of software/database/Web development experience as well as 6 years of experience as a Delphi and Java instructor. He has written numerous games and home entertainment programs and authored articles on genetic algorithms and Java Database Connectivity (JDBC) frameworks. You can read more of Jeff's articles at: http://www.SoftTechDesign.com/media.html.

你可能感兴趣的:(Java Tip 141: Fast math with JNI)