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 调用静态方法
int val = ;
jmethodID method;
jclass cls; cls = (*env)->FindClass(env, "com/ibm/example/TestClass");
if ((*env)->ExceptionCheck(env)) {
return ERR_FIND_CLASS_FAILED;
}
method = (*env)->GetStaticMethodID(env, cls, "setInfo", "(I)V");
if ((*env)->ExceptionCheck(env)) {
return ERR_GET_STATIC_METHOD_FAILED;
}
(*env)->CallStaticVoidMethod(env, cls, method,val);
if ((*env)->ExceptionCheck(env)) {
return ERR_CALL_STATIC_METHOD_FAILED;
}
清单 2. 使用缓存的字段 ID
jfieldID a,b,c,d,e; //其中a,b,c,d,e,f是全局缓存的
int sumValues2(JNIEnv* env, jobject obj, jobject allValues)
{
//其中a,b,c,d,e,f是全局缓存的
jint avalue = (*env)->GetIntField(env, allValues, a);
jint bvalue = (*env)->GetIntField(env, allValues, b);
jint cvalue = (*env)->GetIntField(env, allValues, c);
jint dvalue = (*env)->GetIntField(env, allValues, d);
jint evalue = (*env)->GetIntField(env, allValues, e);
jint fvalue = (*env)->GetIntField(env, allValues, f); return avalue + bvalue + cvalue + dvalue + evalue + fvalue;
}
清单 3. 未缓存字段 ID
int sumValues2(JNIEnv* env, jobject obj, jobject allValues)
{
//a,b,c,d,e,f都是局部的,下次用还要再从java类中找一次
jclass cls = (*env)->GetObjectClass(env, allValues);
jfieldID a = (*env)->GetFieldID(env, cls, "a", "I");
jfieldID b = (*env)->GetFieldID(env, cls, "b", "I");
jfieldID c = (*env)->GetFieldID(env, cls, "c", "I");
jfieldID d = (*env)->GetFieldID(env, cls, "d", "I");
jfieldID e = (*env)->GetFieldID(env, cls, "e", "I");
jfieldID f = (*env)->GetFieldID(env, cls, "f", "I");
jint avalue = (*env)->GetIntField(env, allValues, a);
jint bvalue = (*env)->GetIntField(env, allValues, b);
jint cvalue = (*env)->GetIntField(env, allValues, c);
jint dvalue = (*env)->GetIntField(env, allValues, d);
jint evalue = (*env)->GetIntField(env, allValues, e);
jint fvalue = (*env)->GetIntField(env, allValues, f);
return avalue + bvalue + cvalue + dvalue + evalue + fvalue
}
清单 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. 相同方法的两个版本
jlong getElement(JNIEnv* env, jobject obj, jlongArray arr_j, int index) {
jboolean isCopy;
jlong result;
jlong* buffer_j = (*env)->GetLongArrayElements(env, arr_j, &isCopy);
result = buffer_j[index];
(*env)->ReleaseLongArrayElements(env, arr_j, buffer_j, );
return result;
} jlong getElement2(JNIEnv* env, jobject obj, jlongArray arr_j, int index) {
jlong result;
(*env)->GetLongArrayRegion(env, arr_j, index, , &result);
return result;
}
第一个版本可以生成两个完整的数组副本,而第二个版本则完全没有复制数组。当数组大小为 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. 两个方法版本
/*
* java调用的本地代码时,sumValues1,sumValues2是给java调用的
* 最好将各参数直接传过来而不是传java对象,如果传java对象,
* 那么在本地代码中要findclass,getXXXField,这些动作耗时
*/ int sumValues(JNIEnv* env, jobject obj, jint avalue, jint bvalue, jint cvalue, jint dvalue, jint evalue,
jint fvalue) {
return avalue + bvalue + cvalue + dvalue + evalue + fvalue;
} int sumValues2(JNIEnv* env, jobject obj, jobject allValues) { jint avalue = (*env)->GetIntField(env, allValues, a);
jint bvalue = (*env)->GetIntField(env, allValues, b);
jint cvalue = (*env)->GetIntField(env, allValues, c);
jint dvalue = (*env)->GetIntField(env, allValues, d);
jint evalue = (*env)->GetIntField(env, allValues, e);
jint fvalue = (*env)->GetIntField(env, allValues, f); return avalue + bvalue + cvalue + dvalue + evalue + fvalue;
}
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 allValues,sumValues1()
避免了大量的 JNI 开销。
性能技巧 #4
如果可能,将各参数传递给 JNI 本地代码,以便本地代码回调 JVM 获取所需的数据。
4,错误认定本地代码与 Java 代码之间的界限
本地代码和 Java 代码之间的界限是由开发人员定义的。界限的选定会对应用程序的总体性能造成显著的影响。从 Java 代码中调用本地代码以及从本地代码调用 Java 代码的开销比普通的 Java 方法调用高很多。此外,这种越界操作会干扰 JVM 优化代码执行的能力。举例来说,随着 Java 代码与本地代码之间互操作的增加,实时编译器的效率会随之降低。经过测量,我们发现从 Java 代码调用本地代码要比普通调用多花 5 倍的时间。同样,从本地代码中调用 Java 代码也需要耗费大量的时间。
因此,在设计 Java 代码与本地代码之间的界限时应该最大限度地减少两者之间的相互调用。消除不必要的越界调用,并且应该竭力在本地代码中弥补越界调用造成的成本损失。最大限度地减少越界调用的一个关键因素是确保数据处于 Java/本机界限的正确一侧。如果数据未在正确的一侧,则另一侧访问数据的需求则会持续发起越界调用。
性能技巧 #5
定义 Java 代码与本地代码之间的界限,最大限度地减少两者之间的互相调用。
举例来说,如果我们希望使用 JNI 为某个串行端口提供接口,则可以构造两种不同的接口。第一个版本如清单 6 所示:
清单 6. 到串行端口的接口:版本 1
/**
* Initializes the serial port and returns a java SerialPortConfig objects
* that contains the hardware address for the serial port, and holds
* information needed by the serial port such as the next buffer
* to write data into
*
* @param env JNI env that can be used by the method
* @param comPortName the name of the serial port
* @returns SerialPortConfig object to be passed ot setSerialPortBit
* and getSerialPortBit calls
*/
jobject initializeSerialPort(JNIEnv* env, jobject obj, jstring comPortName); /**
* Sets a single bit in an 8 bit byte to be sent by the serial port
*
* @param env JNI env that can be used by the method
* @param serialPortConfig object returned by initializeSerialPort
* @param whichBit value from 1-8 indicating which bit to set
* @param bitValue 0th bit contains bit value to be set
*/
void setSerialPortBit(JNIEnv* env, jobject obj, jobject serialPortConfig,
jint whichBit, jint bitValue); /**
* Gets a single bit in an 8 bit byte read from the serial port
*
* @param env JNI env that can be used by the method
* @param serialPortConfig object returned by initializeSerialPort
* @param whichBit value from 1-8 indicating which bit to read
* @returns the bit read in the 0th bit of the jint
*/
jint getSerialPortBit(JNIEnv* env, jobject obj, jobject serialPortConfig,
jint whichBit); /**
* Read the next byte from the serial port
*
* @param env JNI env that can be used by the method
*/
void readNextByte(JNIEnv* env, jobject obj); /**
* Send the next byte
*
* @param env JNI env that can be used by the method
*/
void sendNextByte(JNIEnv* env, jobject obj);
清单 7. 到串行端口的接口:版本 2
/**
* Initializes the serial port and returns an opaque handle to a native
* structure that contains the hardware address for the serial port
* and holds information needed by the serial port such as
* the next buffer to write data into
*
* @param env JNI env that can be used by the method
* @param comPortName the name of the serial port
* @returns opaque handle to be passed to setSerialPortByte and
* getSerialPortByte calls
*/
jlong initializeSerialPort2(JNIEnv* env, jobject obj, jstring comPortName); /**
* sends a byte on the serial port
*
* @param env JNI env that can be used by the method
* @param serialPortConfig opaque handle for the serial port
* @param byte the byte to be sent
*/
void sendSerialPortByte(JNIEnv* env, jobject obj, jlong serialPortConfig,
jbyte byte); /**
* Reads the next byte from the serial port
*
* @param env JNI env that can be used by the method
* @param serialPortConfig opaque handle for the serial port
* @returns the byte read from the serial port
*/
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. 创建本地引用
void workOnArray(JNIEnv* env, jobject obj, jarray array) {
jint i;
jint count = (*env)->GetArrayLength(env, array);
for (i = ; i < count; i++) {
jobject element = (*env)->GetObjectArrayElement(env, array, i);
if ((*env)->ExceptionOccurred(env)) {
break;
} /* do something with array element */
}
}
每次调用 GetObjectArrayElement()
时都会为元素创建一个本地引用,并且直到本地代码运行完成时才会释放。数组越大,所创建的本地引用就越多。这些本地引用会在本机方法终止时自动释放。JNI 规范要求各本地代码至少能创建 16 个本地引用。虽然这对许多方法来说都已经足够了,但一些方法在其生存期中却需要更多的本地引用。对于这种情况,您应该删除不再需要的引用,方法是使用 JNI DeleteLocalRef()
调用,或者通知 JVM 您将使用更多的本地引用。
清单 9 向 清单 8 中的示例添加了一个 DeleteLocalRef()
调用,用于通知 JVM 本地引用已不再需要,以及将可同时存在的本地引用的数量限制为一个合理的数值,而与数组的大小无关:
清单 9. 添加 DeleteLocalRef()
void workOnArray(JNIEnv* env, jobject obj, jarray array){
jint i;
jint count = (*env)->GetArrayLength(env, array);
for (i=; i < count; i++) {
jobject element = (*env)->GetObjectArrayElement(env, array, i);
if((*env)->ExceptionOccurred(env)) {
break;
} /* do something with array element */ (*env)->DeleteLocalRef(env, element);
}
}
性能技巧 #7
当本地代码造成创建大量本地引用时,在各引用不再需要时删除它们。
您可以调用 JNI EnsureLocalCapacity()
方法来通知 JVM 您将使用超过 16 个本地引用。这将允许 JVM 优化对该本地代码的本地引用的处理。如果无法创建所需的本地引用,或者 JVM 采用的本地引用管理方法与所使用的本地引用数量之间不匹配造成了性能低下,则未成功通知 JVM 会导致 FatalError
。
性能技巧 #8
如果某本地代码将同时存在大量本地引用,则调用 JNI EnsureLocalCapacity() 方法通知 JVM 并允许它优化对本地引用的处理。
NDK(20)JNI的5大性能缺陷及优化技巧的更多相关文章
- NDK(21)JNI的5大正确性缺陷及优化技巧(注意是正确性缺陷)
转自 : http://www.ibm.com/developerworks/cn/java/j-jni/index.html JNI 编程缺陷可以分为两类: 性能:代码能执行所设计的功能,但运行缓慢 ...
- ASP.NET 性能监控工具和优化技巧
转载自:http://blog.haoitsoft.com/index.php/archives/657 ASP.NET 性能监控工具和优化技巧 发表回复 为了阐明准确甄别性能问题的重要性,下面列举了 ...
- NDK(22)JNI编程如何避免常见缺陷
转自 : http://www.ibm.com/developerworks/cn/java/j-jni/index.html 避免常见缺陷 假设您编写了一些新 JNI 代码,或者继承了别处的某些 J ...
- 15 个有用的 MySQL/MariaDB 性能调整和优化技巧(转载的一篇好文)
MySQL 是一个强大的开源关系数据库管理系统(简称 RDBMS).它发布于 1995 年(20年前).它采用结构化查询语言(SQL),这可能是数据库内容管理中最流行的选择.最新的 MySQL 版本是 ...
- 【MySQL】15个有用的MySQL/MariaDB性能调整和优化技巧
MySQL 是一个强大的开源关系数据库管理系统(简称 RDBMS).它发布于 1995 年(20年前).它采用结构化查询语言(SQL),这可能是数据库内容管理中最流行的选择.最新的 MySQL 版本是 ...
- 15 个有用的 MySQL/MariaDB 性能调整和优化技巧
MySQL 是一个强大的开源关系数据库管理系统(简称 RDBMS).它发布于 1995 年(20年前).它采用结构化查询语言(SQL),这可能是数据库内容管理中最流行的选择.最新的 MySQL 版本是 ...
- java+Mysql大数据的一些优化技巧
众所周知,java在处理数据量比较大的时候,加载到内存必然会导致内存溢出,而在一些数据处理中我们不得不去处理海量数据,在做数据处理中,我们常见的手段是分解,压缩,并行,临时文件等方法; 例如,我们要将 ...
- 网站入住各大搜索引擎的seo优化技巧
最近在公司上班的时候做了一个工业物联网的项目,上层主管提出要求,让这个网站入住各大搜索引擎,也就是说在各大搜索引擎中输入与网站相关的关键字就能搜索到我们自己的网站.刚开始自己一脸懵逼,因为之前自己并没 ...
- Unity3D中使用Profiler精确定位性能热点的优化技巧
本文由博主(SunboyL)原创,转载请注明出处:http://www.cnblogs.com/xsln/p/BeginProfiler.html 简介 在使用Profiler定位代码的性能热点时,很 ...
随机推荐
- bnuoj 33656 J. C.S.I.: P15(图形搜索题)
http://www.bnuoj.com/bnuoj/problem_show.php?pid=33656 [题解]:暴力搜索题 [code]: #include <iostream> # ...
- cadence16.6 中orcad导出网表时ERROR (ORCAP-5004)
ORCAD网表输出时 ERROR (ORCAP-5004):Error initializing COM property pages 之前遇到过这个问题,解决后忘了记录下来了.依稀记得问题答 ...
- CSS去除Chrome浏览器的控件默认样式
html的input输入框在Chrome浏览器里是有默认样式的,当它获得焦点时,即使你没有为它设置:focus时的样式,Chrome浏览器还是会给它加上蓝色的边框,今天百度找到有个方法可以去除该默认样 ...
- For和While在C和MATLAB中的区别——MATLAB的大坑
For和while是常见的循环关键字,在许多语言中都是通用的.但是想必不是所有人,都被其中的区别困扰过,尤其是MATLAB“程序员”. x=[,,,,,,]; i=; while i<=leng ...
- 为什么数据可以从pl/sql查出来而使用ado.net查询,结果却是空?
1.背景 一条记录(如select * from A where a='1'),使用pl/sql作为条件可以查询出记录,但用ado.net sql查询结果却是空. 2.原因 a字段的数据类型的char ...
- Spring+Mybatis+Maven 整合配置
<?xml version="1.0" encoding="UTF-8"?> <beans default-autowire="by ...
- 【WCF--初入江湖】04 WCF通信模式
04 WCF通信模式 WCF的通信模式有三种 [1]请求响应模式: 只能是客户端调用服务器; 客户端请求并等待服务器的响应后才继续执行后续操作(异步调用除外) [2]单工模式: 只能是客户端调用服务器 ...
- POJ1144 Network 无向图的割顶
现在打算重新学习图论的一些基础算法,包括像桥,割顶,双连通分量,强连通分量这些基础算法我都打算重敲一次,因为这些量都是可以用tarjan的算法求得的,这次的割顶算是对tarjan的那一类算法的理解的再 ...
- 在IDEA上用python来连接集群上的hive
1.在使用Python连接hive之前需要将hive中的文件拷贝到自己创建python项目中 cp -r apache-hive--bin/lib/py /home/jia/Desktop 2.把h ...
- hdu2717 Catch That Cow
http://acm.hdu.edu.cn/showproblem.php?pid=2717 //水搜... #include<stdio.h> #include<math.h> ...