您不应该关心字节码,因为现代 JVM 足够聪明,可以以同样有效的方式编译 lookupswitch 和 tableswitch。
直觉上tableswitch 应该更快,这也是由
JVM specification:
因此,tableswitch 指令可能比 lookupswitch 更有效,因为空间考虑允许选择。
但是,该规范是在 20 多年前编写的,当时 JVM 还没有 JIT 编译器。现代 HotSpot JVM 是否存在性能差异?
基准
package bench;
import org.openjdk.jmh.annotations.*;
@State(Scope.Benchmark)
public class SwitchBench {
@Param({"1", "2", "3", "4", "5", "6", "7", "8"})
int n;
@Benchmark
public long lookupSwitch() {
return Switch.lookupSwitch(n);
}
@Benchmark
public long tableSwitch() {
return Switch.tableSwitch(n);
}
}
为了精确控制字节码,我用Jasmin 构建了Switch 类。
.class public bench/Switch
.super java/lang/Object
.method public static lookupSwitch(I)I
.limit stack 1
iload_0
lookupswitch
1 : One
2 : Two
3 : Three
4 : Four
5 : Five
6 : Six
7 : Seven
default : Other
One:
bipush 11
ireturn
Two:
bipush 22
ireturn
Three:
bipush 33
ireturn
Four:
bipush 44
ireturn
Five:
bipush 55
ireturn
Six:
bipush 66
ireturn
Seven:
bipush 77
ireturn
Other:
bipush -1
ireturn
.end method
.method public static tableSwitch(I)I
.limit stack 1
iload_0
tableswitch 1
One
Two
Three
Four
Five
Six
Seven
default : Other
One:
bipush 11
ireturn
Two:
bipush 22
ireturn
Three:
bipush 33
ireturn
Four:
bipush 44
ireturn
Five:
bipush 55
ireturn
Six:
bipush 66
ireturn
Seven:
bipush 77
ireturn
Other:
bipush -1
ireturn
.end method
结果显示 lookupswitch/tableswitch 基准测试之间没有性能差异,但根据 switch 参数存在细微差异:
组装
让我们通过查看生成的汇编代码来验证理论。
以下 JVM 选项将有所帮助:-XX:CompileCommand=print,bench.Switch::*
# {method} {0x0000000017498a48} 'lookupSwitch' '(I)I' in 'bench/Switch'
# parm0: rdx = int
# [sp+0x20] (sp of caller)
0x000000000329b240: sub $0x18,%rsp
0x000000000329b247: mov %rbp,0x10(%rsp) ;*synchronization entry
; - bench.Switch::lookupSwitch@-1
0x000000000329b24c: cmp $0x4,%edx
0x000000000329b24f: je 0x000000000329b2a5
0x000000000329b251: cmp $0x4,%edx
0x000000000329b254: jg 0x000000000329b281
0x000000000329b256: cmp $0x2,%edx
0x000000000329b259: je 0x000000000329b27a
0x000000000329b25b: cmp $0x2,%edx
0x000000000329b25e: jg 0x000000000329b273
0x000000000329b260: cmp $0x1,%edx
0x000000000329b263: jne 0x000000000329b26c ;*lookupswitch
; - bench.Switch::lookupSwitch@1
...
我们在这里看到的是从中间值 4 开始的二分搜索(这解释了为什么案例 4 在上图中具有最佳性能)。
但有趣的是tableSwitch的编译方式完全一样!
# {method} {0x0000000017528b18} 'tableSwitch' '(I)I' in 'bench/Switch'
# parm0: rdx = int
# [sp+0x20] (sp of caller)
0x000000000332c280: sub $0x18,%rsp
0x000000000332c287: mov %rbp,0x10(%rsp) ;*synchronization entry
; - bench.Switch::tableSwitch@-1
0x000000000332c28c: cmp $0x4,%edx
0x000000000332c28f: je 0x000000000332c2e5
0x000000000332c291: cmp $0x4,%edx
0x000000000332c294: jg 0x000000000332c2c1
0x000000000332c296: cmp $0x2,%edx
0x000000000332c299: je 0x000000000332c2ba
0x000000000332c29b: cmp $0x2,%edx
0x000000000332c29e: jg 0x000000000332c2b3
0x000000000332c2a0: cmp $0x1,%edx
0x000000000332c2a3: jne 0x000000000332c2ac ;*tableswitch
; - bench.Switch::tableSwitch@1
...
跳表
但是等等...为什么是二分查找,而不是跳转表?
HotSpot JVM 有一个启发式方法,可以仅为具有 10 多个案例的开关生成跳转表。这可以通过选项-XX:MinJumpTableSize= 进行更改。
好的,让我们用更多标签来扩展我们的测试用例,并再次检查生成的代码。
# {method} {0x0000000017288a68} 'lookupSwitch' '(I)I' in 'bench/Switch'
# parm0: rdx = int
# [sp+0x20] (sp of caller)
0x000000000307ecc0: sub $0x18,%rsp ; {no_reloc}
0x000000000307ecc7: mov %rbp,0x10(%rsp) ;*synchronization entry
; - bench.Switch::lookupSwitch@-1
0x000000000307eccc: mov %edx,%r10d
0x000000000307eccf: dec %r10d
0x000000000307ecd2: cmp $0xa,%r10d
0x000000000307ecd6: jb 0x000000000307ece9
0x000000000307ecd8: mov $0xffffffff,%eax
0x000000000307ecdd: add $0x10,%rsp
0x000000000307ece1: pop %rbp
0x000000000307ece2: test %eax,-0x1faece8(%rip) # 0x00000000010d0000
; {poll_return}
0x000000000307ece8: retq
0x000000000307ece9: movslq %edx,%r10
0x000000000307ecec: movabs $0x307ec60,%r11 ; {section_word}
0x000000000307ecf6: jmpq *-0x8(%r11,%r10,8) ;*lookupswitch
; - bench.Switch::lookupSwitch@1
^^^^^^^^^^^^^^^^^^^^^^^^^
是的!这是我们计算出来的跳转指令。请注意,这是为lookupswitch 生成的。但是tableswitch 会有完全相同的。
令人惊讶的是,HotSpot JVM 甚至可以为带有间隙和异常值的开关生成跳转表。 -XX:MaxJumpTableSparseness 控制间隙的大小。例如。如果有从 1 到 10 的标签,那么从 13 到 20 以及值为 99 的最后一个标签 - JIT 将为值 99 生成保护测试,而对于其余标签,它将创建一个表。
源代码
HotSpot 源代码最终会证明,在使用 C2 对方法进行 JIT 编译后,lookupswitch 和 tableswitch 之间应该没有性能差异。这基本上是因为对两条指令的解析最终都会调用相同的 jump_switch_ranges 函数,该函数适用于任意一组标签。
结论
如我们所见,HotSpot JVM 可以使用二分搜索编译tableswitch,使用跳转表编译lookupswitch,反之亦然。这取决于标签的数量和密度,而不是字节码本身。
所以,回答你原来的问题 - 你不需要!