原文地址: http://agilejava.blogbus.com/logs/39858996.html
最近一直有个问题困扰着我,今天研究了一个晚上,结果从表面上看上说得通,但是也不能确认就是正确的。
事件的起因是近期在搞一个消息处理的功能,要定义大量的消息型,这些消息都是整形的,需要根据消息来判断应该采用哪种处理器进行处理。类似下面的代码:
boolean v = false;
switch (i) {
case 1:
v = true;
break;
case 2:
v = true;
break;
}
这种消息类型有很多,类型数值是连续定义的,可以保证编译生成的jvm指令是tableswitch,这种形式的swtich语句检索效率很高(相应的另一种是lookupswitch,检索使用二分查找,效率要差一些).
因为消息类型很多,要在同一个大方法里写很多的case语句,维护起来不方便,后来就想用Map
但是我担心使用map的检索效率会比switch低,写了个测试进行验证,结果让我很意外,使用map的测试数据总是比switch这种做法要快一些。
测试环境:
JDK1.6 Linux2.6 2G内存
测试数据:
从1~1000个整数中进行查找,即在map中放入从1~1000的整数,而switch方法中相应有1000个case语句,每个case语句对应一个数值.
测试方法:
每轮测试分别查找1~1000,测试100000次,取每轮测试的平均值,结果如下:
$ java -server -Xms512m -Xmx512m -cp . SwitchMapTest
tableswith:56743ns
map:26333ns
这个测试结果很不解,switch语句被编译后,对应得是jvm的tableswitch指令,执行起来也就几条指令就完成了;而HashMap的get操作,下面是jdk的源代码:
public V get(Object key) {
if (key == null)
return getForNullKey();
int hash = hash(key.hashCode());
for (Entry
e != null;
e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
return e.value;
}
return null;
}
map.get所执行的jvm指令肯定是要比tableswitch要多的。
但是测试的情况是map反而比switch要快。
到了这一步,我越来越想不通了。
测试了一下在禁用JIT的情况:
$ java -server -Xms512m -Xmx512m -Xint -cp . SwitchMapTest
tableswith:53036ns
map:533155ns
在禁用JIT以后,map的执行效率大大地下降了,因为我怀疑JIT的优化在这里起到了很重要的作用。
我需要看到JVM在执行的过程中JIT进行优化的情况,上网搜索了一下,http://weblogs.java.net/blog/kohsuke/archive/2008/03/deep_dive_into.html这里有详细的介绍,下载jdk-6u14-ea-bin-b06-linux-amd64-debug-06_may_2009.jar后安装,
使用下面的命令查看JIT优化的日志:
java -server -XX:+PrintOptoAssembly -Xms512m -Xmx512m SwitchMapTest
输出的日志中有详细的优化日志,下面是HashMap.get方法在其中的一部分,
{method}
- klass: {other class}
- method holder: 'java/util/HashMap'
- constants: 0x00007f4e5ece70cf{constant pool}
- access: 0xc1000001 public
- name: 'get'
- signature: '(Ljava/lang/Object;)Ljava/lang/Object;'
- max stack: 3
- max locals: 5
- size of params: 2
- method size: 15
- vtable index: 5
- code size: 79
- code start: 0x00007f4e34c1ff30
- code end (excl): 0x00007f4e34c1ff7f
- method data: 0x00007f4e34dfdd40
- checked ex length: 0
- linenumber start: 0x00007f4e34c1ff7f
- localvar length: 5
- localvar start: 0x00007f4e34c1ff92
因为输出的日志很大,就不在这里贴出来了。
查看优化日志之后发现,使用switch实现的方法并没有被优化,优化全部是针对Map,Integer等进行的,也就是说在使用map的实现中,大量地利用了JIT的本地优化代码;而switch的实现以jvm指令的形式执行,这样解释了为什么在这个测试中map在启用JIT的情况下,会比switch快一倍左右;而禁用JIT以后,会慢10倍左右。
虽然是由于JIT引起的性能差别,但是为什么JIT没有对swtich的实现进行优化?
我能想到的解释就是那个方法太大了,有1000个case语句,JIT忽略了?
下面的测试,我把1000个case语句分解成10个小方法,每个方法有100个case语句,类似下面的代码:
public static void tableswitch(int i) {
boolean v = false;
v = tableswitch_1(i);
if (v)
return;
v = tableswitch_101(i);
if (v)
return;
v = tableswitch_201(i);
if (v)
return;
v = tableswitch_301(i);
if (v)
return;
v = tableswitch_401(i);
if (v)
return;
v = tableswitch_501(i);
if (v)
return;
v = tableswitch_601(i);
if (v)
return;
v = tableswitch_701(i);
if (v)
return;
v = tableswitch_801(i);
if (v)
return;
v = tableswitch_901(i);
if (v)
return;
}
private static boolean tableswitch_1(int i) {
boolean v = false;
switch (i) {
case 1:
v = true;
break;
case 2:
v = true;
break;
case 3:
.....
case 100:
v = true;
break;
}
return v;
}
执行测试:
$ java -server -Xms512m -Xmx512m SwitchMapTest
tableswith:22288ns
map:29169ns
这时switch的实现要比map快一些了,打开优化日志再看:
$ java -server -XX:+PrintOptoAssembly -Xms512m -Xmx512m SwitchMapTest
在输出的日志中可以发现类似下面的内容:
{method}
- klass: {other class}
- method holder: 'SwitchTest'
- constants: 0x00007fc069f040cf{constant pool}
- access: 0x8100000a private static
- name: 'tableswitch_1'
- signature: '(I)Z'
- max stack: 1
- max locals: 2
- size of params: 1
- method size: 15
- vtable index: -2
- code size: 915
- code start: 0x00007fc04000ac30
- code end (excl): 0x00007fc04000afc3
- method data: 0x00007fc04000fdd0
- checked ex length: 0
- linenumber start: 0x00007fc04000afc3
- localvar length: 2
- localvar start: 0x00007fc04000b096
#
所有的10个tableswitch_方法全部被优化了,从而导致switch实现的性能超过了map实现。
到此,我的困惑基本上解开了,从中得到了什么呢?总结如下:
1.Java的性能测试受环境的影响很大
Java性能测试是比较像物理学中的“测不准”。
在测试的时候,-server和-client的表现很可能不一样,而我们的开发机一般默认是client的,如果是在做server上的应用,建议在IDE的jvm配置中默认加上-server的选项。
相同的实现,在server模式中的性能可能就好,而在client中的性能可能就要差一些,所以我们要确定程序是部署在哪种环境下的,从开始就在那个环境中测试并确定解决方案。
2.JIT对性能的提升太恐怖了
JIT在不到万不得已的时候不要禁用。
以前遇到过问题,就是JVM的compiler线程总崩溃,后来不得已禁止了JIT的优化,才正常了,当时是全部给禁了,现在想来可以把引起那个问题的类禁止优化就可以了,否则打击面太大了,性能的损失太大了。
3.Java的方法还是要小一些的好
Java中的方法,尽量避免超级大方法,很多代码在一起。
小方法一是可以使逻辑更清楚,修改、维护起来很方便;另一方面是它有利于JIT的优化,提升性能。第二点我是猜得,记得以前也在网上见到过类似的文章,暂且先相信我吧,呵呵。
写了这么多,也许这个问题不值得这么费劲地去论证,在真实的应用中1000左右的case语句还是很少见的,不过既然研究了,就写在这里,供以后参考也是好的,也不知道得出的结论的对不对,如有错误请指正,谢谢 :)