【问题标题】:Force tableswitch instead of lookupswitch强制使用 tableswitch 而不是 lookupswitch
【发布时间】:2017-12-12 19:38:17
【问题描述】:

Scala 2.11 将相对密集的Int 范围内的match 表达式编译为lookupswitch

lookupswitch { // 21
    -12: 200
    -11: 200
    -10: 184
     -9: 190
     -8: 190
     -7: 190
     -6: 190
     -5: 190
     -4: 200
     -1: 200
      2: 195
      3: 195
      4: 195
      5: 195
      6: 184
      7: 184
     12: 184
     13: 184
     18: 184
     21: 184
     25: 184
default: 180
}

而 Java 7 将等效的 switch 语句编译成 tableswitch

tableswitch { // -12 to 25
    -12: 168
    -11: 168
    -10: 177
     -9: 174
     -8: 174
     -7: 174
     -6: 174
     -5: 174
     -4: 168
     -3: 185
     -2: 185
     -1: 168
      0: 185
      1: 185
      2: 171
      3: 171
      4: 171
      5: 171
      6: 177
      7: 177
      8: 185
      9: 185
     10: 185
     11: 185
     12: 181
     13: 181
     14: 185
     15: 185
     16: 185
     17: 185
     18: 181
     19: 185
     20: 185
     21: 181
     22: 185
     23: 185
     24: 185
     25: 181
default: 185
}

有没有办法强制 Scala 也生成 tableswitch

【问题讨论】:

  • 你为什么在乎?在我看来,Scala 的方式更好,因为它生成的代码更小。在性能方面,两种变体都是相同的。 JIT 编译器将为lookupswitchtableswitch 生成同样优化的代码。
  • 实际上,HotSpot JVM 可以使用二分搜索编译tableswitch,使用跳转表编译lookupswitch,反之亦然。这取决于标签的数量和密度,而不是字节码本身。
  • @apangin Official Oracle docs state “因此,在空间考虑允许选择的情况下,tableswitch 指令可能比 lookupswitch 更有效。”这是过时的信息吗?如果您可以提供生成的本机程序集的答案或其他东西来证明您的观点,我很乐意投票并接受...
  • 该语句仍然适用于解释执行或一般不太优化的 JVM。但这没什么好担心的,除非您遇到性能问题,而像分析器这样的客观工具真正可以追溯到 switch 指令。
  • @apangin 我同意性能无关紧要,但 Java 的方式实际上是“更小”的字节码。这是违反直觉的,因为 javap 打印出开关的方式,但 tableswitch 指令最终是 164-167 字节(取决于对齐方式),而 lookupswitch 是 176-179 字节(取决于对齐方式)。这是因为lookupswitch 需要将每个密钥存储为一个 4 字节的 int,而密钥隐含在 lookuptable 中。

标签: java scala jvm switch-statement pattern-matching


【解决方案1】:

您不应该关心字节码,因为现代 JVM 足够聪明,可以以同样有效的方式编译 lookupswitchtableswitch

直觉上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 编译后,lookupswitchtableswitch 之间应该没有性能差异。这基本上是因为对两条指令的解析最终都会调用相同的 jump_switch_ranges 函数,该函数适用于任意一组标签。

结论

如我们所见,HotSpot JVM 可以使用二分搜索编译tableswitch,使用跳转表编译lookupswitch,反之亦然。这取决于标签的数量和密度,而不是字节码本身。

所以,回答你原来的问题 - 你不需要!

【讨论】:

  • 刚刚发现您的答案,因为它在某些评论中被用作“测试”......很棒的工作。我希望我在这里的第 25 次投票也能为此获得徽章。太棒了。
【解决方案2】:

编辑:您可以通过像这样注释匹配来检查表或查找开关的创建:

import scala.annotation.switch

(foo: @switch) match {
  case 0 =>
  case 1 =>
  //And so forth
}

如果给定的匹配项无法编译到表或查找开关中,这将使其在编译时记录警告。

【讨论】:

  • "要应用于匹配表达式的注释。如果存在,编译器将验证匹配是否已编译为 tableswitch 或 lookupswitch,如果匹配则发出错误而是编译成一系列条件表达式。”
猜你喜欢
  • 2012-05-04
  • 1970-01-01
  • 2012-08-17
  • 2020-03-08
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多