最近有不少人提出 Java 循环优化问题,问题分为两类:
1 )
for ( int i=0; i<10000; i--){ 。。。 }
与
for ( int i = 100000; i > 0; i--){ 。。。 }
这个比较无非是 i++ 和 i— 的比较。
2 )
for ( int i=0; i<1000; i++) {
for ( int j=0; j<100000; j++) {
。。。
}
}
与
for ( int i = 0; i < 100000; i++) {
for ( int j = 0; j < 1000; j++) {
。。。
}
}
这个比较主要问题在于循环次数多的放在里面还是外面的问题。
我的观点:首先,这种代码上自以为是的优化是没有意义的;其次,拿 C 语言的思维来考虑这个问题表示 Java 基本常识都不懂。
分析:
在具体阐明我的观点前有必要做一点 Java 常识的普及。(注,若没有特别说明,文中 sun 的指的是被 Oracle 收购的那个 sun ,也用来表示 Oracle 接手的 sun 公司的一些产品,如 sun 的 jvm ,既指 Oracle 接手 sun 后的虚拟机,也指未收购时的 sun 的虚拟机)
1) jvm 。
众所周知,这是 java 虚拟机。但是,很多人,包括初学者甚至一些工作了的人,对 jvm 的认识仅仅是 sun 的 hotspot 虚拟机,就是从 sun 官网上下载的那个。
而实际上, sun 公司制定的是一个规范,即 Java虚拟机规范 ,搜索 jvmspec 即可得。同时 sun 也提供了该规范的一个标准实现,就是 sun 的 hotspot 虚拟机( N 年前的版本就不说了)。但很多人不知道的是,除了 sun 实现了虚拟机外,还有很多公司也根据自己的需要实现了 Java 虚拟机,比较常见的有 IBM 的 J9 ( Websphere 中用的), Oracle 的 JRockit ( Weblogic 中用的), Apache 的 Harmony (由于利益等原因, sun 和 Oracle 都没有给它提供兼容性测试),还有 openJDK 。这些都是比较流行的。诸如此类,还有很多很多。
2) Java 指令集
jvm 规范中,为 Java 定义了一套指令集。如 iadd , iinc 等,指令集用单字节表示,也就是说不超过 255 个。
关于规范中 jvm 指令集最需要注意的一点是:指令集指描述了指令该做什么事情,对于如何去做,是留给 jvm 实现者自己去思考的,所以不同的 jvm 实现对于同一段代码在效率上可能会有很大的差别。譬如,对于 iadd 指令,两个 int 相加,既可以直接交给硬件去做,也可以拐弯抹角的去做,只要最终结果符合 jvm 规范的描述即可。
3) Java 栈
需要知道的是, Java 考虑到跨平台的需要,所有指令的操作都不是基于寄存器的,而是内存中的 Java 栈。在 C 语言中,有个寄存器用于 pc 计数器,而在 Java 中, pc 计数器是内存中的一个字。 Java 栈由栈帧组成,栈帧分为局部变量区,操作数栈和帧数据区。 jvm 指令的操作数大都源于操作数栈。这与一些语言从寄存器中取操作数是不同的。而局部变量区和操作数栈的大小在编译 Java 文件时就已经确定了。
对于问题一,我们有必要看一下 Java 中对于 i++ 和 i— 所使用的指令
public class Test {
public static void main(String... args) {
int i = 0;
i++;
i--;
}
}
上面的代码编译后再用 javap –c Test 查看用到的指令:
Compiled from "Test.java"
public class Test extends java.lang.Object{
public Test();
Code:
0: aload_0
1: invokespecial #1; //Methodjava/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: iconst_0
1: istore_1
2: iinc 1, 1
5: iinc 1, -1
8: return
}
从上面我们发现 i++ 和 i-- 其实用的是同一个指令,即 iinc ,不过操作数不一样罢了;该指令直接修改局部变量区的值,而不需要压栈。至于如何去实现这个指令,不同的人在实现 jvm 的时候有自己的想法。所以问题一的比较是毫无意义的。你在 jvm 实现 1 上运行很快,可能在 jvm 实现 2 上面就运行很慢。
对于问题二,如上面普及常识所言,不同的 jvm 可以有不同的实现,不同的优化。与 C 语言不同之处在于, java 的局部变量区在运行一个方法的时候已经分配好了,不需要遇到一个新变量就去分配。另外一点,因为由 jvm 来执行,这种循环是 jvm 内部是存在优化余地的,譬如,对于里层的循环变量,在重新下一次循环时就没用了,一些 jvm 实现就可以重用这个变量。一些 JVM 可能是纯粹的解释执行字节码,一些 JVM 可能在启动的时候就把字节码编译成 c++ 本地代码,一些 jvm 可能用纯硬件芯片来执行指令集,还有一些 jvm 在运行了一段时间后找出程序热区,仔细优化并将这部分代码编译成 c++ 本地代码来达到最佳效果。所有你能想到的优化都可以在这里做掉。所以不同的 jvm 实现对于这样的代码效率可能会有很大的差别。
经过本人实测(循环体是数值计算), IBM J9 1.6 中两种都很快, Sun hotspot 跟 J91.6 相比不是一个数量级的。但是 IBM J9 1.5 的实现则相当的慢,跟 hotspot 比慢的不是一个数量级。有兴趣的可能分别试试 openJDK , IBM J9 , Jrockit 这些实现的 1.5 和 1.6 版本的效率,另外一些 jvm 还有 server 和 client 运行版本的区别,效率也是大不一样的
总结,隔了一层虚拟机,什么都有可能!