感谢同事 {空蒙}的投稿
最近碰到个场景,还蛮有普遍性的,如mtop的请求入口非常多,有api2,api3,api4,h5,h5update,cdn_cache,bigpipe等,而Mtop需要依据其具体的入口,选择不同的业务逻辑进行对应的处理。
马上想到两个方案:
- 方案一:采用map存放对应入口的处理方法,然后请求进来后经过get就行,map.get(et);
- 方案二:采用switch语句。
1 |
switch (et) { |
2 |
case API3: |
3 |
return api3service; |
4 |
case API4: |
5 |
return api4service; |
6 |
case API2: |
7 |
return api2service; |
8 |
…… |
if else这种就不予考虑了,明显采用map显的更优雅,代码更具可维护性,目前mtop存在6个入口,api4还未上,如果用switch每次需要硬编码那性能呢?
但用map,也可以做些优化处理,比如我发现api3、h5、cdn_cache在map默认大小为16下,其桶位置发生了碰撞,这样每次get的时候就需要遍历了,这是不好,当然有两种方案,一是改key值避免碰撞,二是改map大小,让其不发生碰撞,我采用map大小为64,避免碰撞,当然后面如要继续添加时候,需要关注经测试,性能可以提升44%,(本机场景,并且这个key在桶的最尾部,也就是需要全部遍历桶全部数据的场景,并且全部预先执行1w次,摒弃了jit对结果的影响)
但map的get操作,每次需要进行hash,位移操作,&操作,再比较操作,想想就需要很多的指令要执行完才能拿到
如果是switch呢?Switch在编译后,有LookupSwitch 和 TableSwitch,其中TableSwitch是O(1)的,LookupSwitch是 O(log n),TableSwitch情况如下:
1 |
int chooseNear( int i) { |
2 |
switch (i) { |
3 |
case 0 : return 0 ; |
4 |
case 1 : return 1 ; |
5 |
case 2 : return 2 ; |
6 |
default : return - 1 ; |
7 |
} |
8 |
} |
编译后结果
01 |
Method int chooseNear( int ) |
02 |
0 iload_1 // Push local variable 1 (argument i) |
03 |
1 tableswitch 0 to 2 : // Valid indices are 0 through 2 |
04 |
0 : 28 // If i is 0, continue at 28 |
05 |
1 : 30 // If i is 1, continue at 30 |
06 |
2 : 32 // If i is 2, continue at 32 |
07 |
default : 34 // Otherwise, continue at 34 |
08 |
28 iconst_0 // i was 0; push int constant 0... |
09 |
29 ireturn // ...and return it |
10 |
30 iconst_1 // i was 1; push int constant 1... |
11 |
31 ireturn // ...and return it |
12 |
32 iconst_2 // i was 2; push int constant 2... |
13 |
33 ireturn // ...and return it |
14 |
34 iconst_m1 // otherwise push int constant -1... |
15 |
35 ireturn // ...and return it |
也就是TableSwitch只要计算一次偏移量,立即就能到case执行,其时间复杂度为O(1)
1 |
LookupSwitch |
2 |
int chooseFar( int i) { |
3 |
switch (i) { |
4 |
case - 100 : return - 1 ; |
5 |
case 0 : return 0 ; |
6 |
case 100 : return 1 ; |
7 |
default : return - 1 ; |
8 |
} |
9 |
} |
编译后:
01 |
Method int chooseFar( int ) |
02 |
0 iload_1 |
03 |
1 lookupswitch 3 : |
04 |
- 100 : 36 |
05 |
0 : 38 |
06 |
100 : 40 |
07 |
default : 42 |
08 |
36 iconst_m1 |
09 |
37 ireturn |
10 |
38 iconst_0 |
11 |
39 ireturn |
12 |
40 iconst_1 |
13 |
41 ireturn |
14 |
42 iconst_m1 |
15 |
43 ireturn |
也就是LookupSwitch编译后会保证其顺序,并采用二分法查找对应的case,其时间复杂度为O(log n)
本机,全部预先执行1w次跳过jit的影响,采用map与switch各执行1亿次,执行时间是两位数的差距,map为400多ms,switch为5ms
当然测试的场景case都比较少,如果达到1k多个case条件呢? Jit还会把jvm指令缓存吗?,如果不缓存又是另外的情况了
大家可以把eclipse设置Xint,看看屏蔽jit后大量运行的效果
还有switch在什么场景下编译后会是TableSwitch,什么下会是LookupSwitch,毕竟两者的时间复杂度还是有差距
Java应用的性能,还是要详细分析其场景,至于要性能还是代码更优雅,要自己权衡了,呵呵,有更好的方案,还请分享哦