Java优化策略(一):底层优化

By 冲出宇宙 From 傲尔科技(www.hour41.com)
时间:2006-11-17
注:转载请注明作者和单位。

       Java语言标准从1996年发布第一版,到2000年发布第二版,再到2004年发布第三版,java语言已经经过了3次大的扩充和改进。功能是越来越多,操作是越来越复杂。显然,性能问题也就越来越突出。本文将力图从java本身分析优化java代码的各种可能。文章的数据未经特别说明,均来自于jdk5.0版本。

1 Java基本数据类型时间分析

       因为单独测试一个基本类型的变量很不精确,无论你怎么测,误差都很大。所以,我们只能大致的给出速度上的差别。

       变量的访问速度和访问的类型是有很大关系的,一般我们直接使用2个变量的操作时间来衡量变量的访问速度。显然,byte + byte的速度和int + int的速度肯定是不同的。我们对以下几类基本操作进行了速度测试:加减(+,-)、乘(*)、除(/)、移位(>>,<<,>>>,<<<)和布尔运算(|,&,^)。为了保证数据的有效性,测试得到了3份结果,取平均值,得到下面的各个基本数据类型的大致时间花销比例如下表(以下均以此作为参考对象,一个时间单位在我的机器上大约是600ps,即0.6ns,也就是6e-10秒):

 

 

 

 

加减

移位

布尔运算

byte

1.5

6

50

1.5

1

short

1.2

5.5

50

1.2

1

int

1

5

48

1

1

long

5

10

140

8.5

2

float

15

600

350

-

-

double

15

600

350

-

-

       从表中我们可以看出:

1.         int的运算速度最快,short次之,byte再次之,long再次之。float和double运算速度最慢。

2.         除法比乘法慢的太多,基本上除法是乘法的9倍时间。当然,除了浮点型外。根据intel cpu的参考数据,乘法计算时间是移位运算的4-5倍是比较正常的。

3.         long类型的计算很慢,建议一般少使用它。

4.         double运算速度和float相当;

5.         浮点的乘法比除法要慢。但是,这个结果并不能真正说明问题。这个结果只是一个一般性的,在特殊情况下,乘法还是比除法快,比如:floatA * floatB 仍然是比floatA / (1/floatB)快。从实际的数据结果来讲,乘法的时候,乘数越小速度越快,特别是在乘数比3小的时候,乘法时耗接近20,大于4的时候,几乎都是600的时耗。除法恰好相反,除数大于1的时候,时耗一般都是350,可是,当除数小于1的时候,时耗就变成了700了。

对于大家关心的移位和乘除2的问题,jdk5.0已经做了部分处理。即“var *=2”和“var <<=1”耗费一样。但是,除法并没有进行这类处理,即“var /= 2”耗费和基本的除法一样。

2 Java类和接口调用时间分析

2.1 类的创建  

虽然面向对象思想已经深入人心,但他在带来快捷方面的编程风格的时候,也带来了低下的效率。在Java中,反应最快的是Object类(这也是显然的),建立一个新的Object类时耗仅仅为20单位。而一个空类(即没有声明任何MethodsFields)的建立时间则增加到了惊人的400单位。如果再给类增加一些字段的话,时间耗费并没有特别大的增加,每增加一个int类型字段大概增加30个单位。

       仅仅就创建时间来说,内嵌的类型都有不错的表现。比如,创建一个int数组(仅仅包含一个元素)的时间只比创建一个Object对象的时间多一倍。当然,如果你创建的数组对象包含1000个元素的话,其创建时间显然还会加上内存管理的时间了,它的时间大概是1万个时间单位。请注意,我们这里讨论的时间单位其实十分小,1万个时间单位也仅仅只是有0.006毫秒(0.000006秒)。创建一个byteshortintlongfloatdouble数组对象的时间消耗几乎是一样的。

2.2 方法的调用

       Java在这个方面有一点做得很好,就是调用一个只有很少量代码的方法的时耗和直接把这段代码写到本地的时耗相差很小。当然不包括需要分配很多本地变量的情况。

       调用本类(this指针)的方法是最快的,时间在1-2个单位。调用其它类的静态方法也很快,速度和调用本来方法差不多。调用其它类的非静态方法速度就慢一些,在1.5-2.5个时间单位之间。

       调用继承接口的方法是十分缓慢的,是调用普通方法的3倍。但是,如果在实现接口的时候加上final关键字的话,调用这个方法的时耗就和普通方法差不多了。

       最慢的是已经同步化了的方法。即加上了synchronized关键字的方法。调用它的时耗比普通方法高出了近20倍。如果不是万不得已,不要把synchronized加到方法上面,实在不行的话,你可以把它加到代码块上去,这种加法比直接加到方法上面快一点。

       注意,因为方法大部分时候都是完成很多事情的,所以,十分注意调用方法的开销是没有必要的,因为这个时间和方法执行需要的时间比较起来只是毛毛雨。

3 Java基本操作时间耗费

3.1 基本语句

       for循环一次的时间耗费在5个单位左右,本地int变量赋值一次的时间耗费在1-2个单位。下表列出了各种操作的时间耗费:

操作

时间耗费

int var = var

1.5

int array[0] = array[0]

4

for

6

throw --- catch

5000

3.2 Cast的时间耗费

       下表是各种类型之间转化的时间耗费:

转化形式

时耗

SubClass = (SubClass) SuperClass

4

Interface = (Interface) Class

4

int -> byte, int-> char, int-> short, int-> long

1

int->float, int->double

3

int<- long

10-15

int<- float, int<- double

15-20

long<- float, long<- double

30-40

基本优化策略

       基本优化策略的天字第一条就是:不要优化!优化代码会使得代码难以理解,代码之间的逻辑结构会变得十分混乱。所以,优化大师们的建议就是:不到万不得已,千万不要优化你的代码。可惜的是,在搜索引擎方面,处处都是效率的考虑。

       基本的优化策略有以下几条:

1.         选定优化目标。优化不是对所有代码进行优化,这是因为优化代码的代价很高。优化只需要针对少部分代码进行。在这里,90/10或者80/20规则发挥着重要作用。找到代码中最影响速度的地方,然后把块骨头啃下来!

2.         操作符简约。换一个操作符代替原有操作符。这里说的最主要例子就是:用“>>=n”替换“/= 2^n”和“<<=n”替换“*=2^n”。Java用不着第2种替换。

3.         共同表达是提取。提取2个表达式中相同的部分,避免同一个式子重复计算。比如:double x = d * a*b;

double y = e * a*b;

就可以化为:

c = a*b;

x = d*c;

y = e*c;

4.         代码移动。把那些在运算中不改变值的变量放到前面先计算好。比如:

for(int i=0;i<x.length;i++)< font="" style="">

{

       x[i] *= Math.PI * Math.cos(y);

}

就可以修改为:

double d = Math.PI * Math.cos(y);

for(int i=0;i<x.length;i++)< font="" style="">

{

       x[i] *= d;

}

5.         分解循环。循环可以简化代码,同时,他们也增加了额外的循环开销。可以通过减少循环次数或者取消循环来获得更好的性能。比如:

for(int i=0;i<1000000;i++)

{

       x[i] = i;

}

就能够变成:

for(int i=0;i<1000000;i+=2)

{

       x[i] = i;

       x[i+1] = i+1;

}

6.         循环的替换。一般认为把“for(i=0;i<n;i++)< font="" style="">”替换成“for(i=n;--i>=0;)”会有更好的效果。不过,我们的测试结果显示,在JDK5.0的环境下面,这种方式几乎没有任何速度提升。

7.         取消for判断。把“for(i=0;i<n;i++)< font="" style="">”转化为“for(i=0;;i++)”,如果里面的代码能够在i>=n的时候出错的话,这段循环就能够自己结束(通过Exception)。可是,测试的结果说明,只有在n特别大的时候,比如1000万,这种方式才能提高速度。其依据是:exception特别耗时。

5 Java的优化建议

       虽然很多基本的优化策略对JDK5.0并不起作用,我们还是可以根据以上对JDK5.0性能的分析得到一些明显的优化建议的。列举如下:

1.         使用平行数组代替值类。平行数组的概念见http://lotusroots.bokee.com/5787315.html。值类的意思是说没有方法的类或者Beans类。

2.         如果可以,不要使用接口;万一必须使用接口,请在继承的类的方法上面加上final关键字。

3.         方法尽量加上static关键字。这样不仅仅是为了调用速度,更能减少空间占用。

4.         使用已经存在的对象来生成新对象。不要用手动初始化的方式生成对象的拷贝。

5.         避免生成新的对象。

6.         严格控制synchronized关键字的使用。

7.         利用System.arraycopy拷贝数组。

8.         使用int,使用int/long代替浮点,用乘不用除,多用布尔运算。

最后的建议是:使用最新的JDK!! 为什么这么说呢?因为我们测试JDK6.0beta2的速度比JDK5.0快了近15%,这是对基本数据类型进行的运算的测试结果。

posted @ 2012-06-26 10:34  Qiengo  阅读(528)  评论(0编辑  收藏  举报