JNI、その前に

SennaのJavaバインディングがリリースされたらしい。素晴らしい。で、ソースコードを見てみた。JNI だ。懐かしい。

JNI のよくある間違い



  1. メソッドを呼び出すたびに FindClass() や GetMethodID() している。もしくはフィールド値を取り出すたびに GetFieldID() している。



    JNI遅えよ」と文句いう人の大半がこれを犯している。両方ともキャッシュできるのに。。。


    FindClass() の結果に関しては NewGlobalRef() でグローバル参照に一旦しないといけないけど。ただ、単にキャッシュすればいいというものではなく、ロードの順序だとか、クラスローダのコンテキストも必要なら頭に入れておかなければならないので注意です。



    ちなみに、グローバル参照は作りすぎると劇遅になる実装がたしかあったはずなので (ソース失念)、程度問題ですよね。オブジェクトのライフサイクルをよく見極めて、って感じでしょう。


    関連:「Traps and Pitfalls: 10.7 Caching Field and Method IDs




  2. FindClass() で取得した jclass を DeleteLocalRef() し忘れている。


    FindClass() は java.lang.Class.findClass() 呼び出しを簡便にしたメソッドと考えるのが妥当です。




  3. GetStringChars() の戻り値は NTS (Null-Terminated String) とは限らない。


    GetStringUTFChars() の戻り値は、関数の仕様としてではありませんが、JNI における全般の規約として「modified UTF-8 文字列は 0-terminated」という了解があり、NTS であることが事実上保証されています*1。一方、GetStringChars() に関してはあくまで公開されている仕様では NTS であるかどうかは保証されていません。


    関連:「Traps and Pitfalls: 10.8 Terminating Unicode Strings



GetStringUTFLength() を GetStringUTFChars() の前に呼ぶ方がその逆より速い。 (これはうそっぽいことが判明…詳しくは下記参照)

あと実装依存の TIPS として、GCJ と Sun JVM 共通ですが、 GetStringUTFLength() を GetStringUTFChars() よりも前に呼ぶ方が圧倒的にパフォーマンスが向上します。理由は不明ですが。


追記:おそらく CPU のキャッシュのおかげ。GetStringUTFChars() が内部的に GetStringUTFLength() を呼び、確保すべきバッファ長を取得していると思われる (少なくとも gcj ではそう)。GetStringUTFLength() の結果は VM 的にはキャッシュされていないようだ。マルチスレッドで走らせた場合の結果を後でポストします。


むしろ、NTS であることが分かっているならば GetStringUTFChars() の結果に対して strlen() するのがベストアプローチでした。



Test.c:

#include <jni.h>
#include <stdio.h>

#ifdef __cplusplus
extern "C" {
#endif

JNIEXPORT void JNICALL Java_Test_test(JNIEnv* jenv, jclass klass, jstring str)
{
	int i;

	for (i = 0; i < 10; i++) {
		const char *cstr;
		jsize len;
		cstr = (*jenv)->GetStringUTFChars(jenv, str, NULL); // (1)
		len = (*jenv)->GetStringUTFLength(jenv, str);       // (2)
		printf("%s (%d)\n", cstr, len);
		(*jenv)->ReleaseStringUTFChars(jenv, str, cstr);
	}
}

#ifdef __cplusplus
} // extern "C"
#endif

Test.java:

public class Test {
	public static native void test(String a);

	public static void main(String[] args) {
		System.loadLibrary("Test");

		StringBuffer long_string_buf = new StringBuffer(65536);
		for (int i = 0; i < 16384; i++) {
			long_string_buf.append("ABCD");
		}

		final String long_string = long_string_buf.toString();

		for (int i = 0; i < 300; i++) {
			test(long_string);
		}
	}
}

GCJ のバージョン:

java version "1.4.2"
gij (GNU libgcj) version 4.1.2 20061115 (prerelease) (Debian 4.1.1-20)

Copyright (C) 2006 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

Sun J2RE のバージョン:

java version "1.4.2_14"
Java(TM) 2 Runtime Environment, Standard Edition (build 1.4.2_14-b05)
Java HotSpot(TM) Client VM (build 1.4.2_14-b05, mixed mode)

とりあえず (1) に GetStringUTFChars()、(2) に GetStringUTFLength() をおいた場合の結果

% # GCJ
% env LD_LIBRARY_PATH=. time /usr/lib/jvm/java-1.4.2-gcj-4.1-1.4.2.0/bin/java -classpath . Test > /dev/null
5.06user 0.10system 0:05.21elapsed 98%CPU (0avgtext+0avgdata 0maxresident)k
0inputs+0outputs (0major+5540minor)pagefaults 0swaps

% # Sun J2RE
% env LD_LIBRARY_PATH=. time /tmp/j2re1.4.2_14/bin/java -classpath . Test > /dev/null
7.34user 0.13system 0:07.52elapsed 99%CPU (0avgtext+0avgdata 0maxresident)k
0inputs+0outputs (1major+2099minor)pagefaults 0swaps

GCJ 速いッ…!!!!!

今度は (1) に GetStringUTFLength()、(2) に GetStringUTFChars() をおいた場合の結果

% # GCJ
% env LD_LIBRARY_PATH=. time /usr/lib/jvm/java-1.4.2-gcj-4.1-1.4.2.0/bin/java -classpath . Test > /dev/null
4.62user 0.11system 0:04.87elapsed 97%CPU (0avgtext+0avgdata 0maxresident)k
0inputs+0outputs (0major+5539minor)pagefaults 0swaps

% # Sun J2RE
% env LD_LIBRARY_PATH=. time /tmp/j2re1.4.2_14/bin/java -classpath . Test > /dev/null
6.82user 0.14system 0:07.19elapsed 96%CPU (0avgtext+0avgdata 0maxresident)k
0inputs+0outputs (1major+2097minor)pagefaults 0swaps

どうしてか詳細は不明です。

追記: CPU のアーキテクチャも関係するかと思って職場の Power PCMac OS X でもやってみた。
コード中の

#include <jni.h>

#include <JavaVM/jni.h>

に修正し、

gcc -Wall -dynamiclib -o libTest.jnilib test.c  -framework JavaVM

でコンパイルできます。

結果:

% # 最初のパターン
% env DYLD_LIBRARY_PATH=. time java -classpath . Test >/dev/null
        3.20 real         2.41 user         0.81 sys

% # 2番目のパターン
% env DYLD_LIBRARY_PATH=. time java -classpath . Test >/dev/null
        3.15 real         2.39 user         0.79 sys

結果変わらず...。

というわけで半ばデマだったようです。すみませんでした。

*1:一方、ヌル文字は2バイトで表現されます。http://java.sun.com/docs/books/jni/html/types.html#58973 を参照