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

原文地址: 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 = 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语句还是很少见的,不过既然研究了,就写在这里,供以后参考也是好的,也不知道得出的结论的对不对,如有错误请指正,谢谢 :)

你可能感兴趣的:(java)