来源:
http://lotusroots.bokee.com/5886635.html
By 冲出宇宙 From 傲尔科技 (www.hour41.com)
时间: 2006-11-17
注:转载请注明作者和单位。
Java 语言标准从 1996 年发布第一版,到 2000 年发布第二版,再到 2004 年发布第三版, java 语言已经经过了 3 次大的扩充和改进。功能是越来越多,操作是越来越复杂。显然,性能问题也就越来越突出。本文将力图从 java 本身分析优化 java 代码的各种可能。文章的数据未经特别说明,均来自于 jdk5.0 版本。
因为单独测试一个基本类型的变量很不精确,无论你怎么测,误差都很大。所以,我们只能大致的给出速度上的差别。
变量的访问速度和访问的类型是有很大关系的,一般我们直接使用 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 ” 耗费和基本的除法一样。
虽然面向对象思想已经深入人心,但他在带来快捷方面的编程风格的时候,也带来了低下的效率。在 Java 中,反应最快的是 Object 类(这也是显然的),建立一个新的 Object 类时耗仅仅为 20 单位。而一个空类(即没有声明任何 Methods 和 Fields )的建立时间则增加到了惊人的 400 单位。如果再给类增加一些字段的话,时间耗费并没有特别大的增加,每增加一个 int 类型字段大概增加 30 个单位。
仅仅就创建时间来说,内嵌的类型都有不错的表现。比如,创建一个 int 数组(仅仅包含一个元素)的时间只比创建一个 Object 对象的时间多一倍。当然,如果你创建的数组对象包含 1000 个元素的话,其创建时间显然还会加上内存管理的时间了,它的时间大概是 1 万个时间单位。请注意,我们这里讨论的时间单位其实十分小, 1 万个时间单位也仅仅只是有 0.006 毫秒( 0.000006 秒)。创建一个 byte 、 short 、 int 、 long 、 float 和 double 数组对象的时间消耗几乎是一样的。
Java 在这个方面有一点做得很好,就是调用一个只有很少量代码的方法的时耗和直接把这段代码写到本地的时耗相差很小。当然不包括需要分配很多本地变量的情况。
调用本类( this 指针)的方法是最快的,时间在 1-2 个单位。调用其它类的静态方法也很快,速度和调用本来方法差不多。调用其它类的非静态方法速度就慢一些,在 1.5-2.5 个时间单位之间。
调用继承接口的方法是十分缓慢的,是调用普通方法的 3 倍。但是,如果在实现接口的时候加上 final 关键字的话,调用这个方法的时耗就和普通方法差不多了。
最慢的是已经同步化了的方法。即加上了 synchronized 关键字的方法。调用它的时耗比普通方法高出了近 20 倍。如果不是万不得已,不要把 synchronized 加到方法上面,实在不行的话,你可以把它加到代码块上去,这种加法比直接加到方法上面快一点。
注意,因为方法大部分时候都是完成很多事情的,所以,十分注意调用方法的开销是没有必要的,因为这个时间和方法执行需要的时间比较起来只是毛毛雨。
for 循环一次的时间耗费在 5 个单位左右,本地 int 变量赋值一次的时间耗费在 1-2 个单位。下表列出了各种操作的时间耗费:
操作 |
时间耗费 |
int var = var |
1.5 |
int array[0] = array[0] |
4 |
for |
6 |
throw --- catch |
5000 |
下表是各种类型之间转化的时间耗费:
转化形式 |
时耗 |
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< 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 特别耗时。
虽然很多基本的优化策略对 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% ,这是对基本数据类型进行的运算的测试结果。