Java编程优化之旅(一)一般化方法

————————————————————————————————————————   

    "Premature optimization is the root of all evil." 

     ”过早的优化是一切问题的根源“

————————————————————————————————————————

    上面这句话出自《计算机编程艺术卷》的作者高纳德教授,在计算机界大名鼎鼎。。这句话很好理解,在你编程的时候尽量不要在一开始就为了优化它而用尽奇技淫巧。这样往往得不偿失。通常只有当我的程序在成功运行后,然后对效率有很高的要求,但明显自己的程序未达要求的时候,才去进行优化。过早的优化有时会让人十分头疼的。这篇文章讨论的是Java语言本身提高性能的一些小技巧,并不会涉及native代码,或者算法上的优化内容。当然也许你感觉所提升的性能不过是在ms级别的,从而认为它微不足道,然后在如今这个大数据时代,一点点的优化乘以海量的数据,就会有很显著的效果。

    下面进入正题,曾经给我印象很深的一种优化技巧是在《Java性能优化》见到的一个方法:展开循环。然而它真的可行吗?

展开循环?

   
    所谓的展开循环的就是如下面的例子:
for(int i=0;i<9999999;i++)
	array[i]=i;
//展开for循环
for(int i=0;i<9999999;i+=3){
	array[i]=i;
	array[i+1]=i+1;
	array[i+2]=i+2;
}
效率究竟怎么样呢?我们可以写一段简单的代码来测试:
int size = 9999999;//7个9
int [] array = new int[size];
long time = System.currentTimeMillis();

for(int i=0;i<size;i++)
	array[i]=i;
time = System.currentTimeMillis()-time;
System.out.println(time);
time = System.currentTimeMillis();
for(int i=0;i<size;i+=3){
	array[i]=i;
	array[i+1]=i+1;
	array[i+2]=i+2;
}
time = System.currentTimeMillis()-time;
System.out.println(time);
下面是十次运行后打印的结果统计


    可以看出基本上展开之后都会比为展开运行的时间少一些。注意单位是ms。而《Java性能优化》一书中作者的实例提到他运行后时间分别为94ms31ms,可见如今硬件发展之快。。。上面数据中还包括一组“特殊”的数据第7组,你会看到展开之后竟然比上面的多了10ms。当然这个个别数据不具代表性,可忽略。绝大部分还是不会这样的。然而我们同样要看到的是,我们毁掉了代码良好的可读性,如果给其他人看你的代码他会对你的这种写法表现得一头雾水。
下面我们看看如果把size的大小再变大一些:size=99999999时的结果

    可以看出两者的差别在0到20ms以内。。如果你说再把size大小提高一些变成999999999(9个9)。那么对不起,程序报错了,不能分配这么多的空间。展开for循环这种方法还有一点要注意的是,比如上面我举得例子中一个循环体中赋值三次,那么size的大小一定要是3的倍数。如果size不是3的倍数,在写循环体的时候一定要加额外的判断条件来保障不会溢出。所以这种方法在目前来看效率虽有提升然而和他所牺牲掉的可读性相比,诸君还是自己权衡。
    另外,可能还会有人说了“这只是基本数据类型啊,如果是对象的话,性能的差别或许就会更明显了”。对此,我也做了个测试,发现前面这种论调确实有一定道理,不过呢只猜对了一半。。
下面是我写的一个程序:
int size = 9999999;
Integer [] array = new Integer [size];
long time = System.currentTimeMillis();

for(int i=0;i<size;i++)
	array[i]=new Integer(i);
time = System.currentTimeMillis()-time;
System.out.println(time);
time = System.currentTimeMillis();
for(int i=0;i<size;i+=3){
	array[i]=new Integer(i);
	array[i+1]=new Integer(i+1);
	array[i+2]=new Integer(i+1);
}
time = System.currentTimeMillis()-time;
System.out.println(time);
    结果是:第一个5483,第二个6423。是啊对象的话,确实比基本数据类型有较大差别,不过呢,展开之后却比不展开的慢了好多,而不是快了好多。。这当然不是偶然结果。多运行几次结果都是下面的要慢上好多。
    综述:关于展开循环这种优化方法,对于基本数据类型能够有所提升。而对于对象数组来说基本上展开循环这种方法不用考虑了,运行时间不降反增。。

以“不变”应“变” 


    提取公共表达式

    提取公共表达式这一点,十分容易理解,我们所操作的代码中通常会有大量重复的代码,如果可以的话,把其中一些重复的操作提取出来,就会提高效率。举个网上的例子。
double x = d * (lim / max) * sx;
double y = d * (lim / max) * sy;
可以写成
double depth = d * (lim / max);
double x = depth * sx;
double y = depth * sy;
    很好理解吧。如果程序中有大量的重复运算便可以提取出来只运算一次就可以了。

    将重复操作的结果保存起来

     这一点和上面一点基本上大同小异。但是有个情况大家可能会忽略,比如在for循环的时候
for(int i=0;i<array.length;i++){
	array[i]=i;
}

int size = array.length;//保存结果
for(int i=0;i<size;i++){
	array[i]=i;
}
    关于for循环(表达式1;表达式2;表达式3){ 循环体 }我们都知道循环的执行顺序,再每一次循环体执行结束后,都会去执行一次表达式2,来验证是否满足条件,所以如果一个数组的话,那么每一次都会调用一次求长度的操作,无形之中增加了开销。

善用位操作


    位操作并是像复合赋值运算符(+=、-=、/=、*=……)那样的语法糖。它的出现由来已久,不仅在C/C++中,汇编语言中就已存在,它确实是一种效率极高的运算操作。


    用左移、右移代替乘、除

    右移>>n 相当于 /2的n次方。反之 左移<<n相当于*2的n次方。我们来看看效率。
int size = 100000000;
int [] array = new int[size];
int [] array1 = new int[size];

for(int i=0;i<size;i++){
	array[i]=i;
	array1[i]=i;
}
long time = System.currentTimeMillis();

for(int i=0;i<size;i++){
	array[i]>>=8;
}
time = System.currentTimeMillis()-time;
System.out.println(time);

time = System.currentTimeMillis();
for(int i=0;i<size;i++){
	array1[i]/=256;
}
time = System.currentTimeMillis()-time;
System.out.println(time);
其10次运行的结果:
效率可见一斑。

    慎用boolean类型的位操作

    
    Java中的boolean类型已经不同于C++中的bool类型了,任何判断逻辑的地方也不能像C/C++那样,用0为非,非0为真来代替了。也就是说如果一个整型a=0。在C/C++中可以写作  if(a) 来判断a是否为零。但是Java中则不可以要写成 if(a!=0)。然而boolean类型却还是支持两个位操作:按位与 &、按位或 |(不支持按位非~,仅支持逻辑非 !)。其执行效果和 && 与 ||是相同的,在见识到位操作的强大之后,你也许会觉得使用按位与 &、按位或 | 会比逻辑与 && 、逻辑非 !效率更高。比如两个boolean类型的变量a,b,在大量的逻辑判断语句中,if(a&b)是否比 if (a&&b)效率更高呢?
boolean a = true;
boolean b = false;
int size = 1000000000;
long time = System.currentTimeMillis();
for (int i = 0; i < size; i++)
	if(a&b){};

time = System.currentTimeMillis() - time;
System.out.println(time);

time = System.currentTimeMillis();
for (int i = 0; i < size; i++)
	if(a&&b){};
time = System.currentTimeMillis() - time;
System.out.println(time);
其10次输出结果:


可见我们的猜想,是错误的。对于boolean类型逻辑运算的效率要高于位运算。具体原因大概是编译器做了大量的优化工作。    

--------------------------------------------------------------------------------------------------------------------------------
    本文标题为“一般化方法”,也就是说比较简单的,并且并不只针对于Java的一些优化方式,C/C++同样适用。当然C/C++本身效率就很高了,不必过分优化。这篇文章的主要目的是抛砖引玉,以后我会写接着写下去,关于Java优化的技巧。大家共同进步吧!

posted on 2014-03-29 19:41  果冻虾仁  阅读(190)  评论(0编辑  收藏  举报

导航