来源:
http://lotusroots.bokee.com/5886635.html
By
冲出宇宙
From
傲尔科技
(www.hour41.com)
时间:
2006-11-17
注:转载请注明作者和单位。
Java
语言标准从
1996
年发布第一版,到
2000
年发布第二版,再到
2004
年发布第三版,
java
语言已经经过了
3
次大的扩充和改进。功能是越来越多,操作是越来越复杂。显然,性能问题也就越来越突出。本文将力图从
java
本身分析优化
java
代码的各种可能。文章的数据未经特别说明,均来自于
jdk5.0
版本。
1 J
ava
基本数据类型时间分析
因为单独测试一个基本类型的变量很不精确,无论你怎么测,误差都很大。所以,我们只能大致的给出速度上的差别。
变量的访问速度和访问的类型是有很大关系的,一般我们直接使用
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
单位。而一个空类(即没有声明任何
Methods
和
Fields
)的建立时间则增加到了惊人的
400
单位。如果再给类增加一些字段的话,时间耗费并没有特别大的增加,每增加一个
int
类型字段大概增加
30
个单位。
仅仅就创建时间来说,内嵌的类型都有不错的表现。比如,创建一个
int
数组(仅仅包含一个元素)的时间只比创建一个
Object
对象的时间多一倍。当然,如果你创建的数组对象包含
1000
个元素的话,其创建时间显然还会加上内存管理的时间了,它的时间大概是
1
万个时间单位。请注意,我们这里讨论的时间单位其实十分小,
1
万个时间单位也仅仅只是有
0.006
毫秒(
0.000006
秒)。创建一个
byte
、
short
、
int
、
long
、
float
和
double
数组对象的时间消耗几乎是一样的。
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 |
1 |
int |
3 |
int |
10-15 |
int |
15-20 |
long |
30-40 |
4
基本优化策略
基本优化策略的天字第一条就是:不要优化!优化代码会使得代码难以理解,代码之间的逻辑结构会变得十分混乱。所以,优化大师们的建议就是:不到万不得已,千万不要优化你的代码。可惜的是,在搜索引擎方面,处处都是效率的考虑。
基本的优化策略有以下几条:
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< font=""><>
{
x[i] *= Math.PI * Math.cos(y);
}
就可以修改为:
double d = Math.PI * Math.cos(y);
for(int i=0;i< font=""><>
{
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< font=""><>
”替换成“
for(i=n;--i>=0;)
”会有更好的效果。不过,我们的测试结果显示,在
JDK5.0
的环境下面,这种方式几乎没有任何速度提升。
7.
取消
for
判断。把“
for(i=0;i< font=""><>
”转化为“
for(i=0;;i++)
”,如果里面的代码能够在
i>=n
的时候出错的话,这段循环就能够自己结束(通过
Exception
)。可是,测试的结果说明,只有在
n
特别大的时候,比如
1000
万,这种方式才能提高速度。其依据是:
exception
特别耗时。
5 Java
的优化建议
虽然很多基本的优化策略对
JDK5.0
并不起作用,我们还是可以根据以上对
JDK5.0
性能的分析得到一些明显的优化建