NDK(20)JNI的5大性能缺陷及优化技巧

 

  转自 : http://www.ibm.com/developerworks/cn/java/j-jni/index.html

 

JNI 编程缺陷可以分为两类:

  • 性能:代码能执行所设计的功能,但运行缓慢或者以某种形式拖慢整个程序。
  • 正确性:代码有时能正常运行,但不能可靠地提供所需的功能;最坏的情况是造成程序崩溃或挂起。

性能缺陷

程序员在使用 JNI 时的 5 大性能缺陷如下:

1,不缓存方法 ID、字段 ID 和类

  要访问 Java 对象的字段并调用它们的方法,本机代码必须调用 FindClass()GetFieldID()GetMethodId() 和GetStaticMethodID()。对于 GetFieldID()GetMethodID() 和 GetStaticMethodID(),为特定类返回的 ID 不会在 JVM 进程的生存期内发生变化。但是,获取字段或方法的调用有时会需要在 JVM 中完成大量工作,因为字段和方法可能是从超类中继承而来的,这会让 JVM 向上遍历类层次结构来找到它们。由于 ID 对于特定类是相同的,因此您只需要查找一次,然后便可重复使用。同样,查找类对象的开销也很大,因此也应该缓存它们。

  举例来说,清单 1 展示了调用静态方法所需的 JNI 代码:

性能技巧 #1

查找并缓存常用的类、字段 ID 和方法 ID。
清单 1. 使用 JNI 调用静态方法
 1 int val = 1;
 2 jmethodID method;
 3 jclass cls;
 4 
 5 cls = (*env)->FindClass(env, "com/ibm/example/TestClass");
 6 if ((*env)->ExceptionCheck(env)) {
 7     return ERR_FIND_CLASS_FAILED;
 8 }
 9 method = (*env)->GetStaticMethodID(env, cls, "setInfo", "(I)V");
10 if ((*env)->ExceptionCheck(env)) {
11     return ERR_GET_STATIC_METHOD_FAILED;
12 }
13 (*env)->CallStaticVoidMethod(env, cls, method,val);
14 if ((*env)->ExceptionCheck(env)) {
15     return ERR_CALL_STATIC_METHOD_FAILED;
16 }
清单 2. 使用缓存的字段 ID
jfieldID a,b,c,d,e; //其中a,b,c,d,e,f是全局缓存的
 1 int sumValues2(JNIEnv* env, jobject obj, jobject allValues)
 2 {
 3     //其中a,b,c,d,e,f是全局缓存的
 4     jint avalue = (*env)->GetIntField(env, allValues, a);
 5     jint bvalue = (*env)->GetIntField(env, allValues, b);
 6     jint cvalue = (*env)->GetIntField(env, allValues, c);
 7     jint dvalue = (*env)->GetIntField(env, allValues, d);
 8     jint evalue = (*env)->GetIntField(env, allValues, e);
 9     jint fvalue = (*env)->GetIntField(env, allValues, f);
10 
11     return avalue + bvalue + cvalue + dvalue + evalue + fvalue;
12 }
清单 3. 未缓存字段 ID
 1 int sumValues2(JNIEnv* env, jobject obj, jobject allValues) 
 2 {
 3     //a,b,c,d,e,f都是局部的,下次用还要再从java类中找一次
 4     jclass cls = (*env)->GetObjectClass(env, allValues);
 5     jfieldID a = (*env)->GetFieldID(env, cls, "a", "I");
 6     jfieldID b = (*env)->GetFieldID(env, cls, "b", "I");
 7     jfieldID c = (*env)->GetFieldID(env, cls, "c", "I");
 8     jfieldID d = (*env)->GetFieldID(env, cls, "d", "I");
 9     jfieldID e = (*env)->GetFieldID(env, cls, "e", "I");
10     jfieldID f = (*env)->GetFieldID(env, cls, "f", "I");
11     jint avalue = (*env)->GetIntField(env, allValues, a);
12     jint bvalue = (*env)->GetIntField(env, allValues, b);
13     jint cvalue = (*env)->GetIntField(env, allValues, c);
14     jint dvalue = (*env)->GetIntField(env, allValues, d);
15     jint evalue = (*env)->GetIntField(env, allValues, e);
16     jint fvalue = (*env)->GetIntField(env, allValues, f);
17     return avalue + bvalue + cvalue + dvalue + evalue + fvalue
18 }

 

清单 2  用 3,572 ms 运行了 10,000,000 次。清单 3 用了 86,217 ms — 多花了 24 倍的时间。 

2,触发数组副本

  JNI 在 Java 代码和本地代码之间提供了一个干净的接口。为了维持这种分离,数组将作为不透明的句柄传递,并且本地代码必须回调 JVM 以便使用 set 和 get 调用操作数组元素。Java 规范让 JVM 实现者决定让这些调用提供对数组的直接访问,还是返回一个数组副本。举例来说,当数组经过优化而不需要连续存储时,JVM 可以返回一个副本。(参见 参考资料 获取关于 JVM 的信息)。

  随后,这些调用可以复制被操作的元素。举例来说,如果您对含有 1,000 个元素的数组调用 GetLongArrayElements(),则会造成至少分配或复制 8,000 字节的数据(每个 long 1,000 元素 * 8 字节)。当您随后使用 ReleaseLongArrayElements() 更新数组的内容时,需要另外复制 8,000 字节的数据来更新数组。即使您使用较新的 GetPrimitiveArrayCritical(),规范仍然准许 JVM 创建完整数组的副本。

性能技巧 #2

获取和更新仅本地代码需要的数组部分。在只要数组的一部分时通过适当的 API 调用来避免复制整个数组。GetTypeArrayRegion() 和 SetTypeArrayRegion() 方法允许您获取和更新数组的一部分,而不是整个数组。通过使用这些方法访问较大的数组,您可以确保只复制本地代码将要实际使用的数组部分。

 

举例来说,考虑相同方法的两个版本,如清单 4 所示:

清单 4. 相同方法的两个版本
 1 jlong getElement(JNIEnv* env, jobject obj, jlongArray arr_j, int index) {
 2     jboolean isCopy;
 3     jlong result;
 4     jlong* buffer_j = (*env)->GetLongArrayElements(env, arr_j, &isCopy);
 5     result = buffer_j[index];
 6     (*env)->ReleaseLongArrayElements(env, arr_j, buffer_j, 0);
 7     return result;
 8 }
 9 
10 jlong getElement2(JNIEnv* env, jobject obj, jlongArray arr_j, int index) {
11     jlong result;
12     (*env)->GetLongArrayRegion(env, arr_j, index, 1, &result);
13     return result;
14 }

  第一个版本可以生成两个完整的数组副本,而第二个版本则完全没有复制数组。当数组大小为 1,000 字节时,运行第一个方法 10,000,000 次用了 12,055 ms;而第二个版本仅用了 1,421 ms。第一个版本多花了 8.5 倍的时间!

  另一方面,如果您最终要获取数组中的所有元素,则使用 GetTypeArrayRegion() 逐个获取数组中的元素是得不偿失的。要获取最佳的性能,应该确保以尽可能大的块的来获取和更新数组元素。如果您要迭代一个数组中的所有元素,则 清单 4 中这两个 getElement() 方法都不适用。比较好的方法是在一个调用中获取大小合理的数组部分,然后再迭代所有这些元素,重复操作直到覆盖整个数组。

性能技巧 #3
在单个 API 调用中尽可能多地获取或更新数组内容。如果可以一次较多地获取和更新数组内容,则不要逐个迭代数组中的元素。 

3,回访而不是传递参数

  在调用某个方法时,您经常会在传递一个有多个字段的对象以及单独传递字段之间做出选择。在面向对象设计中,传递对象通常能提供较好的封装,因为对象字段的变化不需要改变方法签名。但是,对于 JNI 来说,本地代码必须通过一个或多个 JNI 调用返回到 JVM 以获取需要的各个字段的值。这些额外的调用会带来额外的开销,因为从本地代码过渡到 Java 代码要比普通方法调用开销更大。因此,对于 JNI 来说,本地代码从传递进来的对象中访问大量单独字段时会导致性能降低。

  考虑清单 5 中的两个方法,第二个方法假定我们缓存了字段 ID:

清单 5. 两个方法版本 
 1 /*
 2  * java调用的本地代码时,sumValues1,sumValues2是给java调用的
 3  * 最好将各参数直接传过来而不是传java对象,如果传java对象,
 4  * 那么在本地代码中要findclass,getXXXField,这些动作耗时
 5  */
 6 
 7 int sumValues(JNIEnv* env, jobject obj, jint avalue, jint bvalue, jint cvalue, jint dvalue, jint evalue,
 8         jint fvalue) {
 9     return avalue + bvalue + cvalue + dvalue + evalue + fvalue;
10 }
11 
12 int sumValues2(JNIEnv* env, jobject obj, jobject allValues) {
13 
14     jint avalue = (*env)->GetIntField(env, allValues, a);
15     jint bvalue = (*env)->GetIntField(env, allValues, b);
16     jint cvalue = (*env)->GetIntField(env, allValues, c);
17     jint dvalue = (*env)->GetIntField(env, allValues, d);
18     jint evalue = (*env)->GetIntField(env, allValues, e);
19     jint fvalue = (*env)->GetIntField(env, allValues, f);
20 
21     return avalue + bvalue + cvalue + dvalue + evalue + fvalue;
22 }

  sumValues2() 方法需要 6 个 JNI 回调,并且运行 10,000,000 次需要 3,572 ms。其速度比sumValues1() 慢 6 倍,后者只需要 596 ms。通过传递 JNI 方法所需的数据[jint avalue, jint bvalue, jint cvalue, jint dvalue, jint evalue,jint fvalue]而不是传jobject allValuessumValues1()避免了大量的 JNI 开销。 

性能技巧 #4
如果可能,将各参数传递给 JNI 本地代码,以便本地代码回调 JVM 获取所需的数据。 

4,错误认定本地代码与 Java 代码之间的界限

  本地代码和 Java 代码之间的界限是由开发人员定义的。界限的选定会对应用程序的总体性能造成显著的影响。从 Java 代码中调用本地代码以及从本地代码调用 Java 代码的开销比普通的 Java 方法调用高很多。此外,这种越界操作会干扰 JVM 优化代码执行的能力。举例来说,随着 Java 代码与本地代码之间互操作的增加,实时编译器的效率会随之降低。经过测量,我们发现从 Java 代码调用本地代码要比普通调用多花 5 倍的时间。同样,从本地代码中调用 Java 代码也需要耗费大量的时间。

  因此,在设计 Java 代码与本地代码之间的界限时应该最大限度地减少两者之间的相互调用。消除不必要的越界调用,并且应该竭力在本地代码中弥补越界调用造成的成本损失。最大限度地减少越界调用的一个关键因素是确保数据处于 Java/本机界限的正确一侧。如果数据未在正确的一侧,则另一侧访问数据的需求则会持续发起越界调用。

性能技巧 #5
定义 Java 代码与本地代码之间的界限,最大限度地减少两者之间的互相调用。

  举例来说,如果我们希望使用 JNI 为某个串行端口提供接口,则可以构造两种不同的接口。第一个版本如清单 6 所示:

清单 6. 到串行端口的接口:版本 1
 1 /**
 2  * Initializes the serial port and returns a java SerialPortConfig objects
 3  * that contains the hardware address for the serial port, and holds
 4  * information needed by the serial port such as the next buffer 
 5  * to write data into
 6  * 
 7  * @param env JNI env that can be used by the method
 8  * @param comPortName the name of the serial port
 9  * @returns SerialPortConfig object to be passed ot setSerialPortBit 
10  *          and getSerialPortBit calls
11  */
12 jobject initializeSerialPort(JNIEnv* env, jobject obj, jstring comPortName);
13 
14 /**
15  * Sets a single bit in an 8 bit byte to be sent by the serial port
16  *
17  * @param env JNI env that can be used by the method
18  * @param serialPortConfig object returned by initializeSerialPort
19  * @param whichBit value from 1-8 indicating which bit to set
20  * @param bitValue 0th bit contains bit value to be set 
21  */
22 void setSerialPortBit(JNIEnv* env, jobject obj, jobject serialPortConfig,
23         jint whichBit, jint bitValue);
24 
25 /**
26  * Gets a single bit in an 8 bit byte read from the serial port
27  *
28  * @param env JNI env that can be used by the method
29  * @param serialPortConfig object returned by initializeSerialPort
30  * @param whichBit value from 1-8 indicating which bit to read
31  * @returns the bit read in the 0th bit of the jint 
32  */
33 jint getSerialPortBit(JNIEnv* env, jobject obj, jobject serialPortConfig,
34         jint whichBit);
35 
36 /**
37  * Read the next byte from the serial port
38  * 
39  * @param env JNI env that can be used by the method
40  */
41 void readNextByte(JNIEnv* env, jobject obj);
42 
43 /**
44  * Send the next byte
45  *
46  * @param env JNI env that can be used by the method
47  */
48 void sendNextByte(JNIEnv* env, jobject obj);
清单 7. 到串行端口的接口:版本 2
 1 /**
 2  * Initializes the serial port and returns an opaque handle to a native
 3  * structure that contains the hardware address for the serial port 
 4  * and holds information needed by the serial port such as 
 5  * the next buffer to write data into
 6  *
 7  * @param env JNI env that can be used by the method
 8  * @param comPortName the name of the serial port
 9  * @returns opaque handle to be passed to setSerialPortByte and 
10  *          getSerialPortByte calls 
11  */
12 jlong initializeSerialPort2(JNIEnv* env, jobject obj, jstring comPortName);
13 
14 /**
15  * sends a byte on the serial port
16  * 
17  * @param env JNI env that can be used by the method
18  * @param serialPortConfig opaque handle for the serial port
19  * @param byte the byte to be sent
20  */
21 void sendSerialPortByte(JNIEnv* env, jobject obj, jlong serialPortConfig,
22         jbyte byte);
23 
24 /**
25  * Reads the next byte from the serial port
26  * 
27  * @param env JNI env that can be used by the method
28  * @param serialPortConfig opaque handle for the serial port
29  * @returns the byte read from the serial port
30  */
31 jbyte readSerialPortByte(JNIEnv* env, jobject obj, jlong serialPortConfig);

  最显著的一个问题就是,清单 6 中的接口在设置或检索每个位,以及从串行端口读取字节或者向串行端口写入字节都需要一个 JNI 调用。这会导致读取或写入的每个字节的 JNI 调用变成原来的 9 倍。第二个问题是,清单 6 将串行端口的配置信息存储在 Java/本机界限的错误一侧的某个 Java 对象上。我们仅在本机侧需要此配置数据;将它存储在 Java 侧会导致本地代码向 Java 代码发起大量回调以获取/设置此配置信息。清单 7 将配置信息存储在一个本机结构中(比如,一个 struct),并向 Java 代码返回了一个不透明的句柄,该句柄可以在后续调用中返回。这意味着,当本地代码正在运行时,它可以直接访问该结构,而不需要回调 Java 代码获取串行端口硬件地址或下一个可用的缓冲区等信息。因此,使用 清单 7 的实现的性能将大大改善。

性能技巧 #6
构造应用程序的数据,使它位于界限的正确的侧,并且可以由使用它的代码访问,而不需要大量跨界调用。 

5,使用大量本地引用而未通知 JVM

  JNI 函数返回的任何对象都会创建本地引用。举例来说,当您调用 GetObjectArrayElement() 时,将返回对数组中对象的本地引用。考虑清单 8 中的代码在运行一个很大的数组时会使用多少本地引用:

清单 8. 创建本地引用
 1 void workOnArray(JNIEnv* env, jobject obj, jarray array) {
 2     jint i;
 3     jint count = (*env)->GetArrayLength(env, array);
 4     for (i = 0; i < count; i++) {
 5         jobject element = (*env)->GetObjectArrayElement(env, array, i);
 6         if ((*env)->ExceptionOccurred(env)) {
 7             break;
 8         }
 9 
10         /* do something with array element */
11     }
12 }

  每次调用 GetObjectArrayElement() 时都会为元素创建一个本地引用,并且直到本地代码运行完成时才会释放。数组越大,所创建的本地引用就越多。这些本地引用会在本机方法终止时自动释放。JNI 规范要求各本地代码至少能创建 16 个本地引用。虽然这对许多方法来说都已经足够了,但一些方法在其生存期中却需要更多的本地引用。对于这种情况,您应该删除不再需要的引用,方法是使用 JNI DeleteLocalRef() 调用,或者通知 JVM 您将使用更多的本地引用。

  清单 9 向 清单 8 中的示例添加了一个 DeleteLocalRef() 调用,用于通知 JVM 本地引用已不再需要,以及将可同时存在的本地引用的数量限制为一个合理的数值,而与数组的大小无关:

清单 9. 添加 DeleteLocalRef()
 1 void workOnArray(JNIEnv* env, jobject obj, jarray array){
 2    jint i;
 3    jint count = (*env)->GetArrayLength(env, array);
 4    for (i=0; i < count; i++) {
 5       jobject element = (*env)->GetObjectArrayElement(env, array, i);
 6       if((*env)->ExceptionOccurred(env)) {
 7          break;
 8       }
 9       
10       /* do something with array element */
11 
12       (*env)->DeleteLocalRef(env, element);
13    }
14 }

 

性能技巧 #7
当本地代码造成创建大量本地引用时,在各引用不再需要时删除它们。

  您可以调用 JNI EnsureLocalCapacity() 方法来通知 JVM 您将使用超过 16 个本地引用。这将允许 JVM 优化对该本地代码的本地引用的处理。如果无法创建所需的本地引用,或者 JVM 采用的本地引用管理方法与所使用的本地引用数量之间不匹配造成了性能低下,则未成功通知 JVM 会导致 FatalError

性能技巧 #8
如果某本地代码将同时存在大量本地引用,则调用 JNI EnsureLocalCapacity() 方法通知 JVM 并允许它优化对本地引用的处理。

 

posted @ 2015-08-22 20:01  f9q  阅读(2867)  评论(0编辑  收藏  举报