JVM的LookupSwitch和TableSwitch之间的区别?

时间:2012-04-23 20:23:51

标签: java bytecode jasmin

我在Java字节码中理解LookUpSwitch和TableSwitch时遇到了一些困难。

如果我理解的话,LookUpSwitch和TableSwitch都对应于Java源代码的switch语句?为什么一个JAVA语句生成2个不同的字节码?

每个Jasmin的文档:

4 个答案:

答案 0 :(得分:83)

不同之处在于,lookupswitch使用带有键和标签的表,但是tableswitch使用仅带标签的表

执行 tableswitch 时,堆栈顶部的int值直接用作表的索引,以获取跳转目标并立即执行跳转。整个查找+跳转过程是 O(1)操作,这意味着它的速度非常快。

执行 lookupswitch 时,将堆栈顶部的int值与表中的键进行比较,直到找到匹配项,然后使用此键旁边的跳转目标执行跳。由于lookupswitch表始终必须排序,因此keyX<每个X< Y,整个查找+跳转过程是 O(log n)操作,因为将使用二进制搜索算法搜索密钥(没有必要将int值与所有可能的密钥进行比较以查找匹配或确定没有任何键匹配)。 O(log n)比O(1)略慢,但它仍然可以,因为许多众所周知的算法都是O(log n),这些通常被认为是快速的;甚至O(n)或O(n * log n)仍然被认为是一个相当不错的算法(慢/坏算法有O(n ^ 2),O(n ^ 3),甚至更糟)。

决定使用哪条指令是由编译器基于 compact switch语句的事实来做出的,例如

switch (inputValue) {
  case 1:  // ...
  case 2:  // ...
  case 3:  // ...
  default: // ...
}

上面的开关非常紧凑,没有数字“孔”。编译器将创建一个这样的表格开关:

 tableswitch 1 3
    OneLabel
    TwoLabel
    ThreeLabel
  default: DefaultLabel

来自Jasmin页面的伪代码很好地解释了这一点:

int val = pop();                // pop an int from the stack
if (val < low || val > high) {  // if its less than <low> or greater than <high>,
    pc += default;              // branch to default 
} else {                        // otherwise
    pc += table[val - low];     // branch to entry in table
}

此代码非常清楚这样的tableswitch是如何工作的。 valinputValuelow为1(交换机中的最低大小值),high为3(交换机中的最高大小值)。

即使有一些孔,开关也可以紧凑,例如

switch (inputValue) {
  case 1:  // ...
  case 3:  // ...
  case 4:  // ...
  case 5:  // ...
  default: // ...
}

上面的开关“几乎紧凑”,它只有一个孔。编译器可以生成以下指令:

 tableswitch 1 6
    OneLabel
    FakeTwoLabel
    ThreeLabel
    FourLabel
    FiveLabel
  default: DefaultLabel

  ; <...code left out...>

  FakeTwoLabel:
  DefaultLabel:
    ; default code

如您所见,编译器必须为2 ,FakeTwoLabel添加假案例。由于2不是交换机的实际值,因此FakeTwoLabel实际上是一个标签,可以在默认情况下确切地改变代码流,因为值2实际上应该执行默认情况。

因此,开关不必非常紧凑,以便编译器创建一个tableswitch,但它至少应该非常接近紧凑性。现在考虑以下开关:

switch (inputValue) {
  case 1:    // ...
  case 10:   // ...
  case 100:  // ...
  case 1000: // ...
  default:   // ...
}

此开关远不及紧凑,它的孔的数量比的数百倍。人们会称之为备用开关。编译器必须生成几千个假案例,以将此开关表示为表格开关。结果将是一个巨大的表,大大夸大了类文件的大小。这不切实际。相反,它会生成一个lookupswitch:

lookupswitch
    1       : Label1
    10      : Label10
    100     : Label100
    1000    : Label1000
    default : DefaultLabel

此表只有5个条目,而不是超过千个条目。该表有4个实数值,O(log 4)为2(log这里记录到2 BTW的基数,而不是10的基数,因为计算机操作二进制数)。这意味着VM最多需要两次比较才能找到inputValue的标签或得出结论,该值不在表中,因此必须执行默认值。即使表有100个条目,VM最多需要7次比较才能找到正确的标签或决定跳转到默认标签(7次比较远不及100次比较,你不觉得吗?)。

因此,这两条指令可以互换或两条指令的原因有历史原因,这是无稽之谈。有两种不同类型情况的指令,一种用于具有紧凑值的开关(用于最大速度),一种用于具有备用值的开关(不是最大速度,但仍然具有良好的速度和非常紧凑的表格表示,无论所有数字孔)。

答案 1 :(得分:10)

什么时候javac 1.8.0_45编译切换到任何一个?

要决定何时使用哪个,您可以使用javac选择算法作为基础。

我们知道langtools的来源位于hg grep -i tableswitch 回购。

然后我们grep:

// Determine whether to issue a tableswitch or a lookupswitch
// instruction.
long table_space_cost = 4 + ((long) hi - lo + 1); // words
long table_time_cost = 3; // comparisons
long lookup_space_cost = 3 + 2 * (long) nlabels;
long lookup_time_cost = nlabels;
int opcode =
    nlabels > 0 &&
    table_space_cost + 3 * table_time_cost <=
    lookup_space_cost + 3 * lookup_time_cost
    ?
    tableswitch : lookupswitch;

,第一个结果是official docs

hi

其中:

  • lo:最大案例值
  • lookup_time_cost = nlabels:最小案例值

因此我们得出结论,它考虑了时间和空间的复杂性,时间复杂度的权重 3

TODO我不明白为什么log(nlabels)而不是tableswitch,因为/([0-9]+\s{0,1}(X|x))|((X|x)\s{0,1}[0-9]+)/g 可以在O(log(n))中使用二分搜索完成。

Bonus:C ++实现也在O(1)跳转表和O(long(n))二进制搜索之间做出类似的选择:langtools/src/share/classes/com/sun/tools/javac/jvm/Gen.java

答案 2 :(得分:5)

Java Virtual Machine Specification描述了差异。 “当开关的情况可以有效地表示为目标偏移表中的索引时,使用tableswitch指令。”规范描述了更多细节。

答案 3 :(得分:0)

我怀疑它主要是历史性的,因为Java字节码的某些特定绑定强调了机器代码(例如Sun自己的CPU)。

tableswitch本质上是一个计算跳转,其中目标是从查找表中获取的。相反,lookupswitch需要比较每个值,基本上是通过表元素的迭代,直到找到匹配值。

显然,这些操作码是可以互换的,但是基于值,一个或另一个可以更快或更紧凑(例如,比较一组中间有大间隙的键和一组连续的键)。