您的位置:首页 > 编程语言 > Java开发

Java性能测试的困惑:switch和map的性能比较

2012-12-06 17:31 302 查看
原文地址: 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<Integer,Handler>这种形式来达到同样的目的。

但是我担心使用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<K,V> e = table[indexFor(hash, table.length)];

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语句还是很少见的,不过既然研究了,就写在这里,供以后参考也是好的,也不知道得出的结论的对不对,如有错误请指正,谢谢 :)
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: