JNI / NDK開発ガイド(9)-JNIコールパフォーマンステストと最適化

前の章では、Javaでネイティブメソッドを宣言し、ローカルインターフェイスの関数プロトタイプ宣言を生成し、C / C ++を使用してこれらの関数を実装し、対応するプラットフォームの動的共有ライブラリを生成して配置する方法を学びました。最後に、Javaプログラムで宣言されたネイティブメソッドを呼び出すと、C /C++で記述された関数が間接的に呼び出されます。C/C++で記述されたプログラムは、JVMの過剰なメモリオーバーヘッドを回避し、高性能を処理できます。コンピューティング、コールシステムサービスおよびその他の機能。同時に、ネイティブコードでJNIが提供するインターフェイスを介して、Javaプログラムのオブジェクトの任意のメソッドとプロパティを呼び出すことも学びました。JNIが提供するいくつかの利点は次のとおりです。しかし、Javaを実行したことのある子供は、JavaプログラムがJVMで実行されることをすべて理解する必要があります。そのため、JavaでC / C ++または他の言語を呼び出す場合、クロスランゲージインターフェイス、またはJNIインターフェイスを介してC /C++コードにアクセスする場合Javaでのオブジェクトのメソッドまたはプロパティは、Javaで独自のメソッドを呼び出す場合と比較されるため、パフォーマンスは非常に低くなります。インターネット上の友人がJavaからローカルインターフェイスを呼び出すための詳細なテストを行いました。JavaはJavaメソッドを呼び出して、プログラムに対するJNIの利点を享受しながら、それによってもたらされるパフォーマンスのオーバーヘッドも受け入れる必要があることを十分に説明しています。以下のセットを参照してください。テストデータ:

JNI空関数を呼び出すJavaとJava空メソッドのパフォーマンステストを呼び出すJava

テスト環境:JDK1.4.2_19、JDK1.5.0_04、JDK1.6.0_14、テストの繰り返し回数は1億回です。テスト結果の絶対値はほとんど重要ではなく、参照用です。JVMとマシンのパフォーマンスに応じて、テストによって生成される値は異なりますが、マシンとJVMが同じ問題に対応できる必要がある場合でも、ネイティブインターフェイスを呼び出すJavaのパフォーマンスは異なりますJavaメソッドを呼び出すJavaよりもはるかに低いです。

Javaの空のメソッドを呼び出すJavaのパフォーマンス:

JDKバージョン JavaはJavaを呼び出します時間のかかる 1秒あたりの平均呼び出し数
1.6 329ms 303951367回
1.5 312ms 320512820回
1.4 312ms 27233115回

JNIの空の関数を呼び出すJavaのパフォーマンス:

JDKバージョン JavaでJNIを調整するのに時間がかかる 1秒あたりの平均呼び出し数
1.6 1531ms 65316786回
1.5 1891ms 52882072回
1.4 3672ms 27233115回

上記のテストデータから、JDKバージョンが高いほど、JNI呼び出しのパフォーマンスが向上することがわかります。JDK1.5では、JNIのパフォーマンスは、空のメソッド呼び出しのみを使用したJava内部呼び出しのパフォーマンスの約5倍遅く、JDK1.4では10倍以上遅くなります。

JNIルックアップメソッドID、フィールドID、クラス参照パフォーマンステスト

Javaオブジェクトのフィールドにアクセスしたり、ネイティブコードでそれらのメソッドを呼び出したりする場合、ネイティブコードはFindClass()、GetFieldID()、GetStaticFieldID、GetMethodID()、およびGetStaticMethodID()を呼び出す必要があります。GetFieldID()、GetStaticFieldID、GetMethodID()、およびGetStaticMethodID()の場合、特定のクラスに対して返されるIDは、JVMプロセスの存続期間中は変更されません。ただし、フィールドとメソッドはスーパークラスから継承される可能性があるため、フィールドまたはメソッドの呼び出しを取得するには、JVMで多くの作業が必要になる場合があります。これにより、JVMはクラス階層をトラバースしてそれらを見つけます。IDは特定のクラスで同じであるため、一度検索してから再利用するだけで済みます。また、クラスオブジェクトの検索にはコストがかかるため、それらもキャッシュする必要があります。以下は、JNIインターフェースFindClassを呼び出してクラスを検索し、GetFieldIDを呼び出してクラスのフィールドIDを取得し、GetFieldValueを呼び出してフィールドの値を取得するパフォーマンスのテストです。キャッシュとは、1回だけ呼び出されることを意味します。呼び出されない場合、対応するJNIインターフェースが毎回呼び出されます
。java.version= 1.6.0_14
JNIフィールドの読み取り(キャッシュクラス= false、キャッシュフィールドID = false)消費時間:79172ミリ秒あたりの平均秒:1263072
JNIフィールド読み取り(キャッシュクラス= true、キャッシュフィールドID = false)時間:1秒あたり平均25015ミリ秒:3997601
JNIフィールド読み取り(キャッシュクラス= false、キャッシュフィールドID = true)時間:1秒あたり平均50765ミリ秒秒:1969861
JNIフィールド読み取り(キャッシュクラス= true、キャッシュフィールドID = true)時間:2125ミリ秒1秒あたりの平均:47058823 java.version
= 1.5.0_04
JNIフィールド読み取り(キャッシュクラス= false、キャッシュフィールドID = false)消費時間:87109 ms 1秒あたりの平均:1147987
JNIフィールド読み取り(キャッシュクラス= true、キャッシュフィールドID = false)時間:32031 ms 1秒あたりの平均:3121975
JNIフィールド読み取り(キャッシュクラス= false、キャッシュフィールドID = true))時間:51657 ms 1秒あたりの平均:1935846
JNIフィールド読み取り(キャッシュクラス= true、キャッシュフィールドID = true)時間:2187ミリ秒1秒あたり平均:45724737 java.version
= 1.4.2_19
JNIフィールド読み取り(キャッシュクラス= false、キャッシュフィールドID = false )時間:97500ミリ秒/秒平均:1025641
JNIフィールド読み取り(キャッシュクラス= true、キャッシュフィールドID = false)時間:38110ミリ秒/秒:2623983
JNIフィールド読み取り(キャッシュクラス= false、キャッシュフィールドID = true)時間:55204ミリ秒/秒の平均:1811462
JNIフィールドの読み取り(キャッシュクラス= true、キャッシュフィールドID = true)時間:4187ミリ秒/秒の平均:23883448
上記のテストデータによると、クラスとID(属性とメソッドID)を見つけるのに時間がかかります。フィールド値を読み取る時間は、基本的に上記のJNInullメソッドでは1桁です。また、クラスとフィールドを毎回名前で検索すると、パフォーマンスが最大40倍低下します。フィールド値の読み取りのパフォーマンスは数百万単位であり、これは頻繁な対話を伴うJNIアプリケーションでは耐えられません。最も時間がかかるのはクラスを検索することなので、クラスとメンバーIDをネイティブに保存する必要があります。クラスとメンバーIDは一定の範囲内で安定していますが、動的にロードされるクラスローダーでは、保存されたグローバルクラスが失敗するか、クラスローダーがアンロードされない可能性があります。OSGIフレームワークなどのJNIアプリケーションでは、特に注意が必要です。この側面。問題。JDK1.4と1.5および1.6の間のギャップは、フィールド値の読み取りとFieldIDの検索で非常に明白です。しかし、最も時間のかかる検索クラスでは、3つのバージョンの間に明らかな違いはありません。

上記のテストから、メソッドID、フィールドID、およびクラス参照を取得するためにJNIインターフェースを呼び出す場合、キャッシュを使用しないと、パフォーマンスが4倍に低下することがはっきりとわかります。したがって、JNIの開発では、キャッシング・テクノロジーを合理的に使用することで、プログラムのパフォーマンスを大幅に向上させることができます。キャッシュには、使用時のキャッシュとクラスが静的に初期化されたときのキャッシュの2種類があり、主にキャッシュが発生した瞬間に違いがあります。

使用中にキャッシュする

フィールドID、メソッドID、およびクラス参照は、関数で使用されるときにキャッシュされます。以下の例を参照してください。

package com.study.jnilearn;

public class AccessCache {

    private String str = "Hello";

    public native void accessField(); // 访问str成员变量
    public native String newString(char[] chars, int len); // 根据字符数组和指定长度创建String对象

    public static void main(String[] args) {
        AccessCache accessCache = new AccessCache();
        accessCache.nativeMethod();
        char chars[] = new char[7];
        chars[0] = '中';
        chars[1] = '华';
        chars[2] = '人';
        chars[3] = '民';
        chars[4] = '共';
        chars[5] = '和';
        chars[6] = '国';
        String str = accessCache.newString(chars, 6);
        System.out.println(str);
    }

    static {
        System.loadLibrary("AccessCache");
    }
}

javahによって生成されたヘッダーファイル:com_study_jnilearn_AccessCache.h

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_study_jnilearn_AccessCache */
#ifndef _Included_com_study_jnilearn_AccessCache
#define _Included_com_study_jnilearn_AccessCache
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     com_study_jnilearn_AccessCache
 * Method:    accessField
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_com_study_jnilearn_AccessCache_accessField(JNIEnv *, jobject);

/*
 * Class:     com_study_jnilearn_AccessCache
 * Method:    newString
 * Signature: ([CI)Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_com_study_jnilearn_AccessCache_newString(JNIEnv *, jobject,
jcharArray, jint);

#ifdef __cplusplus
}
#endif
#endif

ヘッダーファイルに関数を実装します:AccessCache.c

// AccessCache.c
#include "com_study_jnilearn_AccessCache.h"

JNIEXPORT void JNICALL Java_com_study_jnilearn_AccessCache_accessField
  (JNIEnv *env, jobject obj)
{
    // 第一次访问时将字段存到内存数据区,直到程序结束才会释放,可以起到缓存的作用
    static jfieldID fid_str = NULL;
    jclass cls_AccessCache;
    jstring j_str;
    const char *c_str;
    cls_AccessCache = (*env)->GetObjectClass(env, obj); // 获取该对象的Class引用
    if (cls_AccessCache == NULL) {
        return;
    }

    // 先判断字段ID之前是否已经缓存过,如果已经缓存过则不进行查找
    if (fid_str == NULL) {
        fid_str = (*env)->GetFieldID(env,cls_AccessCache,"str","Ljava/lang/String;");

        // 再次判断是否找到该类的str字段
        if (fid_str == NULL) {
            return;
        }
    }

    j_str = (*env)->GetObjectField(env, obj, fid_str);  // 获取字段的值
    c_str = (*env)->GetStringUTFChars(env, j_str, NULL);
    if (c_str == NULL) {
        return; // 内存不够
    }
    printf("In C:\n str = \"%s\"\n", c_str);
    (*env)->ReleaseStringUTFChars(env, j_str, c_str);   // 释放从从JVM新分配字符串的内存空间

    // 修改字段的值
    j_str = (*env)->NewStringUTF(env, "12345");
    if (j_str == NULL) {
        return;
    }
    (*env)->SetObjectField(env, obj, fid_str, j_str);

    // 释放本地引用
    (*env)->DeleteLocalRef(env,cls_AccessCache);
    (*env)->DeleteLocalRef(env,j_str);
}

JNIEXPORT jstring JNICALL Java_com_study_jnilearn_AccessCache_newString
(JNIEnv *env, jobject obj, jcharArray j_char_arr, jint len)
{
    jcharArray elemArray;
    jchar *chars = NULL;
    jstring j_str = NULL;
    static jclass cls_string = NULL;
    static jmethodID cid_string = NULL;
    // 注意:这里缓存局引用的做法是错误,这里做为一个反面教材提醒大家,下面会说到。
    if (cls_string == NULL) {
        cls_string = (*env)->FindClass(env, "java/lang/String");
        if (cls_string == NULL) {
            return NULL;
        }
    }

    // 缓存String的构造方法ID
    if (cid_string == NULL) {
        cid_string = (*env)->GetMethodID(env, cls_string, "<init>", "([C)V");
        if (cid_string == NULL) {
            return NULL;
        }
    }

    printf("In C array Len: %d\n", len);
    // 创建一个字符数组
    elemArray = (*env)->NewCharArray(env, len);
    if (elemArray == NULL) {
        return NULL;
    }

    // 获取数组的指针引用,注意:不能直接将jcharArray作为SetCharArrayRegion函数最后一个参数
    chars = (*env)->GetCharArrayElements(env, j_char_arr,NULL);
    if (chars == NULL) {
        return NULL;
    }
    // 将Java字符数组中的内容复制指定长度到新的字符数组中
    (*env)->SetCharArrayRegion(env, elemArray, 0, len, chars);

    // 调用String对象的构造方法,创建一个指定字符数组为内容的String对象
    j_str = (*env)->NewObject(env, cls_string, cid_string, elemArray);

    // 释放本地引用
    (*env)->DeleteLocalRef(env, elemArray);

    return j_str;
}

例1.Java_com_study_jnilearn_AccessCache_accessField関数の8行目で、フィールドIDを格納する静的変数fid_strが定義されています。関数が呼び出されるたびに、18行目で最初にフィールドIDがキャッシュされているかどうかが判別されます。キャッシュされていない場合は格納されます。 fid_str。では、変数は次回再度呼び出されたときにすでに値を持っているため、JVMに移動して取得する必要はありません。これは、キャッシュとして機能します。

例2.2つの変数cls_stringとcid_stringは、Java_com_study_jnilearn_AccessCache_newString関数の53行目と54行目で定義されており、それぞれjava.lang.Stringクラスのクラス参照とStringのコンストラクターIDを格納するために使用されます。56行目と64行目では、使用前にキャッシュされているかどうかが判断されます。キャッシュされていない場合は、JNIインターフェイスが呼び出され、JVMからStringのクラス参照とコンストラクターIDが取得され、静的変数に格納されます。次回この関数を呼び出すときは、直接使用することができ、再度探す必要がなく、キャッシュの効果も得られます。誰もが最初の反応でそう思うでしょう。ただし、cls_stringはローカル参照です。メソッドやフィールドIDとは異なり、ローカル参照は関数の終了後にVMによって自動的に解放されます。このとき、cls_stringはワイルドターゲットになります(指定されたメモリスペースが解放されました。ただし、変数の値は解放された後もメモリアドレスであり、NULLではありません)。次に関数Java_com_xxxx_newStringが呼び出されると、無効なローカル参照にアクセスしようとするため、不正なメモリアクセスとプログラムのクラッシュが発生します。したがって、関数で静的キャッシュのローカル参照を使用するのは誤りです。次の記事では、ローカル参照とグローバル参照を紹介します。この問題を防ぐためにグローバル参照を使用してください。注意してください。

クラス静的初期化キャッシュ

クラスのメソッドまたはプロパティを呼び出す前に、Java仮想マシンは最初にクラスがメモリにロードされているかどうかを確認します。ロードされていない場合は、最初にロードされ、次にクラスの静的初期化コードブロックが呼び出されます。クラスは静的に初期化されます。プロセス中にクラスのフィールドIDとメソッドIDを計算してキャッシュすることもお勧めします。以下の例を参照してください。

package com.study.jnilearn;

public class AccessCache {

    public static native void initIDs(); 

    public native void nativeMethod();
    public void callback() {
        System.out.println("AccessCache.callback invoked!");
    }

    public static void main(String[] args) {
        AccessCache accessCache = new AccessCache();
        accessCache.nativeMethod();
    }

    static {
        System.loadLibrary("AccessCache");
        initIDs();
    }
}
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_study_jnilearn_AccessCache */
#ifndef _Included_com_study_jnilearn_AccessCache
#define _Included_com_study_jnilearn_AccessCache
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     com_study_jnilearn_AccessCache
 * Method:    initIDs
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_com_study_jnilearn_AccessCache_initIDs
  (JNIEnv *, jclass);

/*
 * Class:     com_study_jnilearn_AccessCache
 * Method:    nativeMethod
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_com_study_jnilearn_AccessCache_nativeMethod
  (JNIEnv *, jobject);

#ifdef __cplusplus
}
#endif
#endif
// AccessCache.c

#include "com_study_jnilearn_AccessCache.h"

jmethodID MID_AccessCache_callback;

JNIEXPORT void JNICALL Java_com_study_jnilearn_AccessCache_initIDs
(JNIEnv *env, jclass cls)
{
    printf("initIDs called!!!\n");
    MID_AccessCache_callback = (*env)->GetMethodID(env,cls,"callback","()V");
}

JNIEXPORT void JNICALL Java_com_study_jnilearn_AccessCache_nativeMethod
(JNIEnv *env, jobject obj)
{
    printf("In C Java_com_study_jnilearn_AccessCache_nativeMethod called!!!\n");
    (*env)->CallVoidMethod(env, obj, MID_AccessCache_callback);
}

JVMがAccessCache.classをメモリにロードした後、クラスの静的初期化コードブロック、つまり静的コードブロックを呼び出し、最初にSystem.loadLibraryを呼び出してダイナミックライブラリをJVMにロードし、次にネイティブを呼び出します。ローカル関数を呼び出すinitIDsメソッド。Java_com_study_jnilearn_AccessCache_initIDs、この関数にキャッシュする必要のあるIDを取得し、それをグローバル変数に格納します。次にこれらのIDを使用する必要があるときは、18行目のJavaのコールバック関数を呼び出すなど、グローバル変数を直接使用できます。

(*env)->CallVoidMethod(env, obj, MID_AccessCache_callback);

2つのキャッシュ方法の比較

JNIインターフェースの作成時にメソッドとフィールドが配置されているクラスのソースコードを制御できない場合は、使用時にキャッシュを使用する方が合理的です。ただし、クラスが静的に初期化される場合のキャッシュと比較して、使用中のキャッシュを使用することにはいくつかの欠点があります。

  1. 使用する前に、IDまたはクラス参照がキャッシュされているかどうかを毎回確認する必要があります
  2. 使用時にキャッシュされたIDを使用する場合、ネイティブコードがIDの値に依存している限り、クラスはアンロードされないことに注意してください。一方、静的初期化中にキャッシュが発生した場合、クラスがアンロードまたはリロードされるときにIDが再計算されます。なぜなら、クラスが静的に初期化されるときに、クラスのフィールドID、メソッドID、およびクラス参照をキャッシュしてみてください。

おすすめ

転載: blog.csdn.net/shangsongwww/article/details/122493281