OpenJDKのJNI

少し前まではJavaは遅いとかいろいろ言われてきましたが、最近ではC言語と比較しても実行時間が200%程度のロスで済むぐらいまで速くなっていますね。純粋にロジック部分のみならほぼ変わりがないのを考えると、かなり速いといっていいと思います。他のLightweight Languageは500%〜2000%ぐらいかかりますし。

ところで、私は5年ぐらいJavaを使ってきたつもりですが、クラスライブラリはともかくJVMの実装はあまり知らなかったりします。最近ちょっと色々あってパフォーマンスに厳しい環境でJavaを扱うことになりそうなので、Javaの最適化について調べています。

最近はなにかとOSSにするのが流行りのようで、Java VMも昨年、GPLでリリースされました。
なので、どのような実装になっているかをソースコードから見ていきたいと思います。
私はコンパイラや最適化の専門家ではないので、多分に誤りが含まれているかもしれません。

まずはソースコードから。OpenJDK7 b40を使用します。
http://download.java.net/openjdk/jdk7/

参考としては
http://wikis.sun.com/display/HotSpotInternals/Home
など。

GetByteArrayElementsとGetPrimitiveArrayCritical

いきなり筋違いですが、JNIの話です。
JNIはJavaからC言語などnative codeを呼ぶための仕組みです。
当然、データの受け渡しがあるわけですが、Javaの配列をJNIから直接扱うことはできません。データの受け渡しの時に重要になるのが、copyの回数です。
1kBや2kB程度のコピーなら最近のPCではすぐに終わりますが、MBスケールの配列を扱おうとすると、配列のコピーだけで数ms食ってしまいます。なので、最小の回数でJavaから扱えるようにしたいですね。

JNIでデータをやりとりする方法は二つあります。DirectBufferとArrayです。
今回はArrayのやりとりについて。

まずJava JNIのAPIを見てください。JNIの配列操作には、プリミティブ型配列とオブジェクト型配列に対する関数があります。データ転送のために使うのは主にプリミティブ型配列(それもbyteArray)です。

http://java.sun.com/j2se/1.5.0/ja/docs/ja/guide/jni/spec/functions.html#array_operations

C言語側から配列を操作するには、GetByteArrayElementsを使います。
これによって、Javaの配列オブジェクトからCの配列へのポインタが取得できます。

ここで注意する必要があるのは、この関数で返されるポインタは配列オブジェクトの内部データそのものではなく、そのコピーである可能性があるということです。すなわち、Javaからデータを取得する場合には一度、配列がコピーされる可能性があります。
これはisCopy引数がtrueになるかfalseになるかで判定できます。

もし配列がコピーであれば、ReleaseByteArrayElementsで配列を解放する際にデータを書き換えたかを明示する必要があります。そして、配列がコピーであればC言語側の配列をJavaに戻す際に、コピーバック(ようするに配列のコピー)が実行されます。

つまり、JavaからCに配列を取り出す際に1回、CからJavaに配列を戻す際に1回の計2回コピーが実行される可能性があります。無駄です。

さて、OpenJDKのソースコードではどうなっているのでしょうか。
JNIに関する部分は、hotspot/src/share/vm/prims/jni.cppにあります。
GetByteArrayElementsについては、2183行目からです。ソースコードはちょっと長いので載せません。

結論からいうと、このバージョンのGetByteArrayElementsは常に配列をコピーします。可能性どころではありません。常にです。

じゃあGetPrimitiveArrayCriticalはどうかというと、こちらは配列がコピーされずに返される可能性を高める、と書いています。ソースコードでは同じファイルの2542行目からです。
こちらも結論からいうと、このバージョンのGetPrimitiveArrayCriticalは常に配列をコピーしません。可能性どころでは(ry)。同様にReleasePrimitiveArrayCriticalも配列がコピーではないため、mode引数によらず常にコピーなしでJava側に変更が反映されます。

よって、コピー回数としてはGetPrimitiveArrayCriticalで取得した場合は0回で済みます。すばらしい。
ただしGetPrimitiveArrayCriticalはGCスレッドをロックするなどの特徴がありますので、使うときは注意ですね。