This article may contain URLs that were valid when originally published, but now link to sites or pages that no longer exist. To maintain the flow of the article, we've left these URLs in the text, but disabled the links.


MIND


java911@microsoft.com         Download the code (69KB)
Jonathan Locke

Speak Java Like a Native

My Java application is suffering from performance problems. I've been thinking about reworking some key parts in native code. But a friend of mine doesn't think that's the right approach. How do I decide what course of action to take? And if I do choose to write native methods, should I use JNI, RNI, COM, or J/Direct™?

Be careful not to let someone push you in the wrong direction on this one—especially if you don't yet know exactly where your performance problems are. It sounds like you are off to a good start from the way you phrased your question. Sometimes just finding the bottleneck is half the job!
      There are two easy ways to get basic, raw profiling information. Really coarse measurements can be obtained by simply checking the values returned by the System.currentTimeMillis method at different points in your code. But the more common—and generally more useful—source of profiling information is to build your code with debug information by using the compiler's -g flag. Then pass in the -prof switch to the JDK debug Virtual Machine (VM), java_g, when you run the code. Profiling results will be dumped into a file called java.prof.
      At any rate, until you have a solid problem diagnosis, I wouldn't attempt to fix anything. What your friend says may very well be right. But more often than not, advice givers are just quoting traditional wisdom. In your case, traditional hacker wisdom says that ten to twenty percent of your code is responsible for the bulk of your code's execution time. The implied conclusion is that if you simply find a better algorithm for that ten to twenty percent, everything will turn out just fine. Sure, this advice works out on classroom chalkboards (where the values of certain k constants in so-called Big O notation aren't considered to be especially noteworthy). But there are a wide variety of practical reasons why a better algorithm might still fail to live up to real-world expectations.


Java Tip of the Month
The Java import statement doesn't actually do any importing. It's simply a convenient way of referring to classes so you don't have to constantly specify fully qualified class names. For example, the statement import java.util.* causes all occurrences of the class name Vector to be expanded to java.util.Vector. Also, don't forget that java.lang is implicitly imported. This is why you can refer to String instead of java.lang.String.


      So, while smarter coding very often does produce significant and worthwhile performance improvements, it can also fail to pan out due to more practical limits at hand. These limits frequently have little or nothing to do with choice of algorithms. I'm talking about gory performance issues like working set size, available storage, storage access speed, processor characteristics, API design, and code and implementation details at all levels. Some of these issues will be out of your control, but many are not (if you're willing to work hard enough). So your friend's advice to fix your performance problems in pure Java amounts to folk wisdom. Don't get me wrong—folk wisdom is a good thing that is very often right. Just be sure to take it with a grain of salt.
      To answer the second part of your question, if and when you decide to take the native methods plunge, you'll want to consider all four possibilities. If you have existing COM objects that suit your purpose on the Windows® platform, you will find using those objects from Java code to be both easy and expedient. It really should go without saying that your COM native methods will only be as portable as the underlying C++ code. COM objects that build upon lots of Windows API functionality will obviously be less portable than COM objects that rely only on the C standard library. But to set the matter straight, this whole big fuss over the lack of Microsoft® support for JNI is without any real technical merit. The bottom line is that native code (and by this I mean raw machine code like x86 or 68000 code) is inherently nonportable. COM is not responsible for this problem and JNI can't make it go away!
      As far as source code portability goes, JNI in the Microsoft Java VM would certainly be nice, but the issue is really one of porting convenience, not of portability itself. In addition, RNI is roughly twice as fast as JNI. I find a fact like that compelling enough that I probably wouldn't use JNI for performance-critical methods anyway. So if Microsoft wants to provide RNI, J/Direct, and COM instead of JNI, then that is just the way things are going to crumble, cookiewise. And at the end of the day, you and I still have to ship products. And whether we have to write our native methods in RNI and JNI separately or a source code standard emerges is of no real consequence to portability because we ultimately have to ship a separate binary image for each library, each chipset, and each platform.

Java Resource of the Month
If you're on a quest for answers to Java questions, you will definitely want to check out the Java FAQ. Set your coordinates to http://sunsite.unc.edu/javafaq Engage!

      The mechanics of writing COM objects and using them in Java have been well-covered in past issues of MIND and are thoroughly documented on the Microsoft Java Web site (http://www.microsoft.com/java). To keep this column from getting too long, I'd like to focus instead on the other three options you have at hand: JNI, RNI, and J/Direct.
      The MessageBoxTest application shown in Figure 1 demonstrates the basic mechanics of using JNI, RNI, and J/Direct in a Java application. Since the Microsoft Java VM supports RNI and J/Direct, while the Sun/JavaSoft VM supports only JNI, it is necessary to do a version check before attempting any native calls. I have done this by checking the java.vendor system property. If the vendor string contains the word "Microsoft" (case-insensitive), then you're running under the Microsoft VM and the isRNISupported and isJDirectSupported boolean flags will be set. Otherwise, only the isJNISupported boolean flag will be set. Based on these flags, I then call the corresponding messageBox native method for each of the three calling conventions.
      The first call to the MessageBoxTest.messageBox method is executed through J/Direct and doesn't require any C++ glue code at all. The relative ease with which you can call existing native code through J/Direct is its primary advantage over RNI.
      Although there is no actual native glue code, the JavaDoc comment

 /** @dll.import("USER32") */
that precedes the MessageBoxTest.MessageBox method declaration specifies that the call is a J/Direct call to a function of the same name in the native library USER32.DLL. When the Microsoft compiler (JVC build 4336 or greater) encounters this tag, it adds a special attribute to the method in the class file that indicates the method is a J/Direct-native method and should be bound directly to an entrypoint of the given DLL.
      The special attribute in question is compliant with the Java VM's specification for class file formatting and will not prevent a class file from loading under other Java VMs. MessageBoxTest does just this—it mixes J/Direct with RNI and JNI—and it works just fine. If you download the code for this month's column, you should be able to run the MessageBoxTest sample application under jview.exe and java.exe and see for yourself.
      When calling MessageBoxTest.MessageBox, the Microsoft Java VM automatically takes care of translating Java types to native types based on the method's signature. Because the second parameter to the @dll.import statement is "unicode," the MessageBoxW function in USER32.DLL will be called. If that parameter were "ansi," the MessageBoxA function would be called. If the parameter is omitted or "auto" is specified, then the appropriate function for the given version of Windows is called (that is, any Unicode-to-ANSI translations occur automatically). Luckily, the USER32 MessageBox function is one of a scarce few functions that comes in both ANSI and Unicode flavors on all Windows platforms.
      The next call in MessageBoxTest is to JNIMessageBox.messageBox. The Java side here is extremely simple. The messageBox method is declared as native and a static initializer block (which is called when the class is first loaded) loads the native library that contains the definition of the messageBox method. The Java VM automatically appends the right file extension to the parameter to System.loadLibrary, so JNIMessageBox becomes JNIMessageBox.DLL on Windows systems and JNIMessageBox.so on Unix systems. When a call is made to messageBox, the VM creates a mangled name for the method and its signature and uses that name to look up the appropriate DLL entry point.

 //
 // JNIMessageBox.java
 //
 
 public class JNIMessageBox
 {
     static native void messageBox(String title, String message);
     static
     {
         System.loadLibrary("JNIMessageBox");
     }
 }
      After compiling JNIMessageBox.java, you can run the JDK javah utility against the resulting class file to produce a JNI header file that can be used in the C/C++ files with which you build the JNIMessageBox.DLL library. The syntax for this command is:

 javah -jni JNIMessageBox 
The results of that command are shown in Figure 2.
      The C++ implementation of the messageBox native method is shown in Figure 3. The JNI environment pointer is used to access the JNI function GetStringChars, which is in turn used to get the Unicode characters from the string. After calling the Win32® MessageBox function, the jchar pointers retrieved through JNI are released.
      The final call in MessageBoxTest is to RNIMessageBox.messageBox. The Java side of RNIMessageBox is almost exactly identical to the JNIMessageBox example (see Figure 4). The only significant difference is that the RNIMessageBox.h header file is created using Microsoft's msjavah utility and the C++ implementation varies accordingly. Judicious use of preprocessor macros or a Perl script or two might make the differences between RNI and JNI code even smaller.
      So that's the basic mechanics of it. On Microsoft platforms, you have the option to use COM, RNI, or J/Direct. Of those three, I personally favor J/Direct. Using J/Direct on the Windows platform is quite fast and results in less icky C++ glue code hanging around. On non-Microsoft Java platforms, you have no choice; you must code to JNI. The upside to JNI is its widespread availability. The downside to JNI is its performance, which is one of the primary reasons that Microsoft has chosen to stick with RNI and J/Direct.
      Included with the downloadable code for this month's column is a simple benchmark test that makes a million calls to a native method. The method simply returns double its integer argument. The results look like this:

 J:\MIND\latest\Benchmark>runall
 
 JNI: 19000 msec.
 RNI: 8531 msec.
 J/Direct: 7672 msec.
Converting this into calls per second, you'd get the chart shown in Figure 5. As you can see, RNI is more than twice as fast as JNI (in my simple test case anyway).
Figure 5: Native Calls Benchmark
Figure 5: Native Calls Benchmark

      So code carefully and test your code on all platforms that you are going to ship on and you should have no problem with the native code portability on Microsoft's VM or any other.


From the February 1998 issue of Microsoft Interactive Developer.