Javaプログラマーがネイティブメソッドに頼るのはなぜですか? サードパーティのDLLライブラリを使用する場合があります。 他のケースでは、最適化されたCまたはアセンブラーコードにより重要なアルゴリズムを高速化します。 たとえば、ストリーミングメディアの処理、圧縮、暗号化など。
ただし、ネイティブメソッドの呼び出しは無料ではありません。 JNIのオーバーヘッドは、パフォーマンスの向上よりも大きい場合があります。 そしてすべてが含まれているからです:
- スタックフレームの作成。
- ABIに従って引数をシフトします。
- JNIハンドルでリンクをラップ(
jobject
); -
jclass
JNIEnv*
およびjclass
追加の引数をjclass
ます。 -
synchronized
メソッドの場合、モニターのキャプチャとリリース。 - ネイティブ関数の「遅延」リンク。
- メソッドの入り口と出口をトレースします。
- ストリームを
in_Java
状態からin_native
、またはその逆に転送する - セーフポイントを確認してください。
- 考えられる例外の処理。
しかし、多くの場合、ネイティブメソッドは単純です:例外をスローせず、ヒープに新しいオブジェクトを作成せず、スタックをバイパスせず、ハンドルを使用せず、同期されません。 彼らが不必要な行動をしないことは可能ですか?
はい。今日は、単純なJNIメソッドをより速く呼び出すためのHotSpot JVMのドキュメント化されていない機能について説明します。 この最適化はJava 7の最初のバージョンから登場しましたが、驚くべきことですが、誰もそれについて書いていません。
私たちが彼を知っているJNI
たとえば、
byte[]
配列を受け取り、要素の合計を返す単純なネイティブメソッドを考えます。 JNIで配列を操作するには、いくつかの方法があります。
-
GetByteArrayRegion
-Java配列要素をネイティブメモリの指定された場所にコピーします。
GetByteArrayRegionの例JNIEXPORT jint JNICALL Java_bench_Natives_arrayRegionImpl(JNIEnv* env, jclass cls, jbyteArray array) { static jbyte buf[1048576]; jint length = (*env)->GetArrayLength(env, array); (*env)->GetByteArrayRegion(env, array, 0, length, buf); return sum(buf, length); }
-
GetByteArrayElements
も同じです。JVM自体が、要素がコピーされるメモリ領域を割り当てます。 配列の処理が完了したら、ReleaseByteArrayElementsを呼び出す必要があります。
GetByteArrayElementsの例JNIEXPORT jint JNICALL Java_bench_Natives_arrayElementsImpl(JNIEnv* env, jclass cls, jbyteArray array) { jboolean isCopy; jint length = (*env)->GetArrayLength(env, array); jbyte* buf = (*env)->GetByteArrayElements(env, array, &isCopy); jint result = sum(buf, length); (*env)->ReleaseByteArrayElements(env, array, buf, JNI_ABORT); return result; }
- なぜ、アレイのコピーを作成するのですか? ただし、JNIメソッドの実行中にガベージコレクターによって直接移動できるため、Javaヒープ内のオブジェクトをネイティブから直接操作することはできません。 ただし、ヒープ内の配列の直接アドレスを返す
GetPrimitiveArrayCritical
関数がありますが、GetPrimitiveArrayCritical
呼び出す前にGCが動作することをReleasePrimitiveArrayCritical
ます。
GetPrimitiveArrayCriticalの例JNIEXPORT jint JNICALL Java_bench_Natives_arrayElementsCriticalImpl(JNIEnv* env, jclass cls, jbyteArray array) { jboolean isCopy; jint length = (*env)->GetArrayLength(env, array); jbyte* buf = (jbyte*) (*env)->GetPrimitiveArrayCritical(env, array, &isCopy); jint result = sum(buf, length); (*env)->ReleasePrimitiveArrayCritical(env, array, buf, JNI_ABORT); return result; }
クリティカルネイティブ
そして、ここが私たちの秘密のツールです。 外観上は、通常のJNIメソッドのように見えますが、
JavaCritical_
代わりに
Java_
プレフィックスが
Java_
ます。 引数のうち、
jclass
JNIEnv*
と
jclass
しており、
jbyteArray
代わりに2つの引数が渡されます
jbyte* data
配列の長さと
jbyte* data
配列の要素への生のポインタ。 したがって、Critical Nativeメソッドは、高価なJNI関数
GetArrayLength
および
GetByteArrayElements
を呼び出す必要がありません-すぐに配列を操作できます。 このメソッドの期間中、GCは遅延します。
JNIEXPORT jint JNICALL JavaCritical_bench_Natives_javaCriticalImpl(jint length, jbyte* buf) { return sum(buf, length); }
ご覧のとおり、実装に余分なものはありません。
ただし、メソッドがクリティカルネイティブになるには、次の厳しい制限を満たす必要があります。
- メソッドは
static
、synchronized
static
ていない必要があります。 - 引数の中では、プリミティブ型とプリミティブの配列のみがサポートされています。
- クリティカルネイティブはJNI関数を呼び出すことができないため、Javaオブジェクトを割り当てたり、例外をスローしたりできません。
- そして、最も重要なことは、メソッドが実行時にGCをブロックするため、メソッドは短時間で完了するはずです。
Critical Nativesは、ネイティブに実装された暗号化関数の呼び出しを高速化するために、JDKのプライベートHotspot APIとして考案されました。 説明から見つけられる最大値は、バグトラッカーのタスクに関するコメントです 。 重要な機能:
JavaCritical_
関数は、ホット(コンパイル済み)コードからのみ呼び出されるため、
JavaCritical_
実装に加えて、メソッドには「フォールバック」の従来のJNI実装も必要です。 ただし、他のJVMとの互換性のために、これはさらに優れています。
グラム単位はいくつですか?
さまざまな長さ(16、256、4KB、64KB、1MB)の配列の節約量を測定してみましょう。 当然JMHを使用します。
ベンチマーク
@State(Scope.Benchmark) public class Natives { @Param({"16", "256", "4096", "65536", "1048576"}) int length; byte[] array; @Setup public void setup() { array = new byte[length]; } @GenerateMicroBenchmark public int arrayRegion() { return arrayRegionImpl(array); } @GenerateMicroBenchmark public int arrayElements() { return arrayElementsImpl(array); } @GenerateMicroBenchmark public int arrayElementsCritical() { return arrayElementsCriticalImpl(array); } @GenerateMicroBenchmark public int javaCritical() { return javaCriticalImpl(array); } static native int arrayRegionImpl(byte[] array); static native int arrayElementsImpl(byte[] array); static native int arrayElementsCriticalImpl(byte[] array); static native int javaCriticalImpl(byte[] array); static { System.loadLibrary("natives"); } }
結果
Java(TM) SE Runtime Environment (build 1.7.0_51-b13) Java HotSpot(TM) 64-Bit Server VM (build 24.51-b03, mixed mode) Benchmark (length) Mode Samples Mean Mean error Units b.Natives.arrayElements 16 thrpt 5 7001,853 66,532 ops/ms b.Natives.arrayElements 256 thrpt 5 4151,384 89,509 ops/ms b.Natives.arrayElements 4096 thrpt 5 571,006 5,534 ops/ms b.Natives.arrayElements 65536 thrpt 5 37,745 2,814 ops/ms b.Natives.arrayElements 1048576 thrpt 5 1,462 0,017 ops/ms b.Natives.arrayElementsCritical 16 thrpt 5 14467,389 70,073 ops/ms b.Natives.arrayElementsCritical 256 thrpt 5 6088,534 218,885 ops/ms b.Natives.arrayElementsCritical 4096 thrpt 5 677,528 12,340 ops/ms b.Natives.arrayElementsCritical 65536 thrpt 5 44,484 0,914 ops/ms b.Natives.arrayElementsCritical 1048576 thrpt 5 2,788 0,020 ops/ms b.Natives.arrayRegion 16 thrpt 5 19057,185 268,072 ops/ms b.Natives.arrayRegion 256 thrpt 5 6722,180 46,057 ops/ms b.Natives.arrayRegion 4096 thrpt 5 612,198 5,555 ops/ms b.Natives.arrayRegion 65536 thrpt 5 37,488 0,981 ops/ms b.Natives.arrayRegion 1048576 thrpt 5 2,054 0,071 ops/ms b.Natives.javaCritical 16 thrpt 5 60779,676 234,483 ops/ms b.Natives.javaCritical 256 thrpt 5 9531,828 67,106 ops/ms b.Natives.javaCritical 4096 thrpt 5 707,566 13,330 ops/ms b.Natives.javaCritical 65536 thrpt 5 44,653 0,927 ops/ms b.Natives.javaCritical 1048576 thrpt 5 2,793 0,047 ops/ms
小さい配列の場合、JNI呼び出しのコストはメソッド自体の実行時間の何倍にもなります。 数百バイトの配列の場合、オーバーヘッドは有用な作業に匹敵します。 さて、マルチキロバイト配列の場合、呼び出す方法はそれほど重要ではありません-実際にはすべての時間が処理に費やされています。
結論
Critical Nativesは、JDK 7で導入されたHotSpotのプライベートJNI拡張です。特定のルールに従ってJNIのような関数を実装することにより、ネイティブメソッドを呼び出し、ネイティブコードでJava配列を処理するオーバーヘッドを大幅に削減できます。 ただし、Critical Nativeの実行中はGCを起動できないため、長時間にわたる機能の場合、このようなソリューションは機能しません。