码迷,mamicode.com
首页 > 编程语言 > 详细

Java 程序优化:字符串操作、基本运算方法等优化策略(二)

时间:2015-09-25 13:28:15      阅读:267      评论:0      收藏:0      [点我收藏+]

标签:

数据定义、运算逻辑优化

使用局部变量

调用方法时传递的参数以及在调用中创建的临时变量都保存在栈 (Stack) 里面,读写速度较快。其他变量,如静态变量、实例变量等,都在堆 (heap) 中创建,读写速度较慢。清单 12 所示代码演示了使用局部变量和静态变量的操作时间对比。

清单 12. 局部变量 VS 静态变量
public class variableCompare {
public static int b = 0;
 public static void main(String[] args){
 int a = 0;
 long starttime = System.currentTimeMillis();
 for(int i=0;i<1000000;i++){
 a++;//在函数体内定义局部变量
 }
 System.out.println(System.currentTimeMillis() - starttime);
 
 starttime = System.currentTimeMillis();
 for(int i=0;i<1000000;i++){
 b++;//在函数体内定义局部变量
 }
 System.out.println(System.currentTimeMillis() - starttime);
 }
}

运行后输出如清单 13 所示。

清单 13. 运行结果
0
15

以上两段代码的运行时间分别为 0ms 和 15ms。由此可见,局部变量的访问速度远远高于类的成员变量。

位运算代替乘除法

位运算是所有的运算中最为高效的。因此,可以尝试使用位运算代替部分算数运算,来提高系统的运行速度。最典型的就是对于整数的乘除运算优化。清单 14 所示代码是一段使用算数运算的实现。

清单 14. 算数运算
public class yunsuan {
 public static void main(String args[]){
 long start = System.currentTimeMillis();
 long a=1000;
 for(int i=0;i<10000000;i++){
 a*=2;
 a/=2;
 }
 System.out.println(a);
 System.out.println(System.currentTimeMillis() - start);
 start = System.currentTimeMillis();
 for(int i=0;i<10000000;i++){
 a<<=1;
 a>>=1;
 }
 System.out.println(a);
 System.out.println(System.currentTimeMillis() - start);
 }
}

运行输出如清单 15 所示。

清单 15. 运行结果
1000
546
1000
63

两段代码执行了完全相同的功能,在每次循环中,整数 1000 乘以 2,然后除以 2。第一个循环耗时 546ms,第二个循环耗时 63ms。

替换 switch

关键字 switch 语句用于多条件判断,switch 语句的功能类似于 if-else 语句,两者的性能差不多。但是 switch 语句有性能提升空间。清单 16 所示代码演示了 Switch 与 if-else 之间的对比。

清单 16.Switch 示例
public class switchCompareIf {

public static int switchTest(int value){
int i = value%10+1;
switch(i){
case 1:return 10;
case 2:return 11;
case 3:return 12;
case 4:return 13;
case 5:return 14;
case 6:return 15;
case 7:return 16;
case 8:return 17;
case 9:return 18;
default:return -1;
}
}

public static int arrayTest(int[] value,int key){
int i = key%10+1;
if(i>9 || i<1){
return -1;
}else{
return value[i];
}
}

 public static void main(String[] args){
 int chk = 0;
 long start=System.currentTimeMillis();
 for(int i=0;i<10000000;i++){
 chk = switchTest(i);
 }
 System.out.println(System.currentTimeMillis()-start);
 chk = 0;
 start=System.currentTimeMillis();
 int[] value=new int[]{0,10,11,12,13,14,15,16,17,18};
 for(int i=0;i<10000000;i++){
 chk = arrayTest(value,i);
 }
 System.out.println(System.currentTimeMillis()-start);
 }
}

运行输出如清单 17 所示。

清单 17. 运行结果
172
93

使用一个连续的数组代替 switch 语句,由于对数据的随机访问非常快,至少好于 switch 的分支判断,从上面例子可以看到比较的效率差距近乎 1 倍,switch 方法耗时 172ms,if-else 方法耗时 93ms。

一维数组代替二维数组

JDK 很多类库是采用数组方式实现的数据存储,比如 ArrayList、Vector 等,数组的优点是随机访问性能非常好。一维数组和二维数组的访问速度不一样,一维数组的访问速度要优于二维数组。在性能敏感的系统中要使用二维数组,尽量 将二维数组转化为一维数组再进行处理,以提高系统的响应速度。

清单 18. 数组方式对比
public class arrayTest {
 public static void main(String[] args){
 long start = System.currentTimeMillis();
 int[] arraySingle = new int[1000000];
 int chk = 0;
 for(int i=0;i<100;i++){
 for(int j=0;j<arraySingle.length;j++){
 arraySingle[j] = j;
 }
 }
 for(int i=0;i<100;i++){
 for(int j=0;j<arraySingle.length;j++){
 chk = arraySingle[j];
 }
 }
 System.out.println(System.currentTimeMillis() - start);
 
 start = System.currentTimeMillis();
 int[][] arrayDouble = new int[1000][1000];
 chk = 0;
 for(int i=0;i<100;i++){
 for(int j=0;j<arrayDouble.length;j++){
 for(int k=0;k<arrayDouble[0].length;k++){
 arrayDouble[i][j]=j;
 }
 }
 }
 for(int i=0;i<100;i++){
 for(int j=0;j<arrayDouble.length;j++){
 for(int k=0;k<arrayDouble[0].length;k++){
 chk = arrayDouble[i][j];
 }
 }
 }
 System.out.println(System.currentTimeMillis() - start);
 
 start = System.currentTimeMillis();
 arraySingle = new int[1000000];
 int arraySingleSize = arraySingle.length;
 chk = 0;
 for(int i=0;i<100;i++){
 for(int j=0;j<arraySingleSize;j++){
 arraySingle[j] = j;
 }
 }
 for(int i=0;i<100;i++){
 for(int j=0;j<arraySingleSize;j++){
 chk = arraySingle[j];
 }
 }
 System.out.println(System.currentTimeMillis() - start);
 
 start = System.currentTimeMillis();
 arrayDouble = new int[1000][1000];
 int arrayDoubleSize = arrayDouble.length;
 int firstSize = arrayDouble[0].length;
 chk = 0;
 for(int i=0;i<100;i++){
 for(int j=0;j<arrayDoubleSize;j++){
 for(int k=0;k<firstSize;k++){
 arrayDouble[i][j]=j;
 }
 }
 }
 for(int i=0;i<100;i++){
 for(int j=0;j<arrayDoubleSize;j++){
 for(int k=0;k<firstSize;k++){
 chk = arrayDouble[i][j];
 }
 }
 }
 System.out.println(System.currentTimeMillis() - start);
 }
}

运行输出如清单 19 所示。

清单 19. 运行结果
343
624
287
390

第一段代码操作的是一维数组的赋值、取值过程,第二段代码操作的是二维数组的赋值、取值过程。可以看到一维数组方式比二维数组方式快接近一半时间。而对于数组内如果可以减少赋值运算,则可以进一步减少运算耗时,加快程序运行速度。

提取表达式

大部分情况下,代码的重复劳动由于计算机的高速运行,并不会对性能构成太大的威胁,但若希望将系统性能发挥到极致,还是有很多地方可以优化的。

清单 20. 提取表达式
public class duplicatedCode {
 public static void beforeTuning(){
 long start = System.currentTimeMillis();
 double a1 = Math.random();
 double a2 = Math.random();
 double a3 = Math.random();
 double a4 = Math.random();
 double b1,b2;
 for(int i=0;i<10000000;i++){
 b1 = a1*a2*a4/3*4*a3*a4;
 b2 = a1*a2*a3/3*4*a3*a4;
 }
 System.out.println(System.currentTimeMillis() - start);
 }
 
 public static void afterTuning(){
 long start = System.currentTimeMillis();
 double a1 = Math.random();
 double a2 = Math.random();
 double a3 = Math.random();
 double a4 = Math.random();
 double combine,b1,b2;
 for(int i=0;i<10000000;i++){
 combine = a1*a2/3*4*a3*a4;
 b1 = combine*a4;
 b2 = combine*a3;
 }
 System.out.println(System.currentTimeMillis() - start);
 }
 
 public static void main(String[] args){
 duplicatedCode.beforeTuning();
 duplicatedCode.afterTuning();
 }
}

运行输出如清单 21 所示。

清单 21. 运行结果
202
110

两段代码的差别是提取了重复的公式,使得这个公式的每次循环计算只执行一次。分别耗时 202ms 和 110ms,可见,提取复杂的重复操作是相当具有意义的。这个例子告诉我们,在循环体内,如果能够提取到循环体外的计算公式,最好提取出来,尽可能让程序 少做重复的计算。

优化循环

当性能问题成为系统的主要矛盾时,可以尝试优化循环,例如减少循环次数,这样也许可以加快程序运行速度。

清单 22. 减少循环次数
public class reduceLoop {
public static void beforeTuning(){
 long start = System.currentTimeMillis();
 int[] array = new int[9999999];
 for(int i=0;i<9999999;i++){
 array[i] = i;
 }
 System.out.println(System.currentTimeMillis() - start);
}

public static void afterTuning(){
 long start = System.currentTimeMillis();
 int[] array = new int[9999999];
 for(int i=0;i<9999999;i+=3){
 array[i] = i;
 array[i+1] = i+1;
 array[i+2] = i+2;
 }
 System.out.println(System.currentTimeMillis() - start);
}

public static void main(String[] args){
reduceLoop.beforeTuning();
reduceLoop.afterTuning();
}
}

运行输出如清单 23 所示。

清单 23. 运行结果
265
31

这个例子可以看出,通过减少循环次数,耗时缩短为原来的 1/8。

布尔运算代替位运算

虽 然位运算的速度远远高于算术运算,但是在条件判断时,使用位运算替代布尔运算确实是非常错误的选择。在条件判断时,Java 会对布尔运算做相当充分的优化。假设有表达式 a、b、c 进行布尔运算“a&&b&&c”,根据逻辑与的特点,只要在整个布尔表达式中有一项返回 false,整个表达式就返回 false,因此,当表达式 a 为 false 时,该表达式将立即返回 false,而不会再去计算表达式 b 和 c。若此时,表达式 a、b、c 需要消耗大量的系统资源,这种处理方式可以节省这些计算资源。同理,当计算表达式“a||b||c”时,只要 a、b 或 c,3 个表达式其中任意一个计算结果为 true 时,整体表达式立即返回 true,而不去计算剩余表达式。简单地说,在布尔表达式的计算中,只要表达式的值可以确定,就会立即返回,而跳过剩余子表达式的计算。若使用位运算 (按位与、按位或) 代替逻辑与和逻辑或,虽然位运算本身没有性能问题,但是位运算总是要将所有的子表达式全部计算完成后,再给出最终结果。因此,从这个角度看,使用位运算替 代布尔运算会使系统进行很多无效计算。

清单 24. 运算方式对比
public class OperationCompare {
 public static void booleanOperate(){
 long start = System.currentTimeMillis();
 boolean a = false;
 boolean b = true;
 int c = 0;
 //下面循环开始进行位运算,表达式里面的所有计算因子都会被用来计算
 for(int i=0;i<1000000;i++){
 if(a&b&"Test_123".contains("123")){
 c = 1;
 }
 }
 System.out.println(System.currentTimeMillis() - start);
 }
 
 public static void bitOperate(){
 long start = System.currentTimeMillis();
 boolean a = false;
 boolean b = true;
 int c = 0;
 //下面循环开始进行布尔运算,只计算表达式 a 即可满足条件
 for(int i=0;i<1000000;i++){
 if(a&&b&&"Test_123".contains("123")){
 c = 1;
 }
 }
 System.out.println(System.currentTimeMillis() - start);
 }
 
 public static void main(String[] args){
 OperationCompare.booleanOperate();
 OperationCompare.bitOperate();
 }
}

运行输出如清单 25 所示。

清单 25. 运行结果
63
0

实例显示布尔计算大大优于位运算,但是,这个结果不能说明位运算比逻辑运算慢,因为在所有的逻辑与运算中,都省略了表达式“"Test_123".contains("123")”的计算,而所有的位运算都没能省略这部分系统开销。

使用 arrayCopy()

数 据复制是一项使用频率很高的功能,JDK 中提供了一个高效的 API 来实现它。System.arraycopy() 函数是 native 函数,通常 native 函数的性能要优于普通的函数,所以,仅处于性能考虑,在软件开发中,应尽可能调用 native 函数。ArrayList 和 Vector 大量使用了 System.arraycopy 来操作数据,特别是同一数组内元素的移动及不同数组之间元素的复制。arraycopy 的本质是让处理器利用一条指令处理一个数组中的多条记录,有点像汇编语言里面的串操作指令 (LODSB、LODSW、LODSB、STOSB、STOSW、STOSB),只需指定头指针,然后开始循环即可,即执行一次指令,指针就后移一个位 置,操作多少数据就循环多少次。如果在应用程序中需要进行数组复制,应该使用这个函数,而不是自己实现。具体应用如清单 26 所示。

清单 26. 复制数据例子
public class arrayCopyTest {
public static void arrayCopy(){
int size = 10000000;
 int[] array = new int[size];
 int[] arraydestination = new int[size];
 for(int i=0;i<array.length;i++){
 array[i] = i;
 }
 long start = System.currentTimeMillis();
 for(int j=0;j>1000;j++){
 System.arraycopy(array, 0, arraydestination, 0, size);//使用 System 级别的本地 arraycopy 方式
 }
 System.out.println(System.currentTimeMillis() - start);
}

public static void arrayCopySelf(){
int size = 10000000;
 int[] array = new int[size];
 int[] arraydestination = new int[size];
 for(int i=0;i<array.length;i++){
 array[i] = i;
 }
 long start = System.currentTimeMillis();
 for(int i=0;i<1000;i++){
 for(int j=0;j<size;j++){
 arraydestination[j] = array[j];//自己实现的方式,采用数组的数据互换方式
 }
 }
 System.out.println(System.currentTimeMillis() - start);
}

 public static void main(String[] args){
 arrayCopyTest.arrayCopy();
 arrayCopyTest.arrayCopySelf();
 }
}

输出如清单 27 所示。

清单 27. 运行结果
0
23166

上面的例子显示采用 arraycopy 方法执行复制会非常的快。原因就在于 arraycopy 属于本地方法,源代码如清单 28 所示。

清单 28.arraycopy 方法
public static native void arraycopy(Object src, int srcPos, 
Object dest, int destPos, 
int length);

src - 源数组;srcPos - 源数组中的起始位置; dest - 目标数组;destPos - 目标数据中的起始位置;length - 要复制的数组元素的数量。清单 28 所示方法使用了 native 关键字,调用的为 C++编写的底层函数,可见其为 JDK 中的底层函数。

结束语

Java 程序设计优化有很多方面可以入手,作者将以系列的方式逐步介绍覆盖所有领域。本文是该系列的第一篇文章,主要介绍了字符串对象操作相关、数据定义方面的优 化方案、运算逻辑优化及建议,从实际代码演示入手,对优化建议及方案进行了验证。作者始终坚信,没有什么优化方案是百分百有效的,需要读者根据实际情况进 行选择、实践。

Java 程序优化:字符串操作、基本运算方法等优化策略(二)

标签:

原文地址:http://my.oschina.net/u/233752/blog/511027

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!