JVM的LookupSwitch和TableSwitch之间的区别?
我有一些难以理解Java字节码中的LookUpSwitch和TableSwitch。
如果我很好理解,LookUpSwitch和TableSwitch都对应于Java源代码的switch
语句? 为什么一个JAVA语句生成2个不同的字节码?
每个Jasmin文档:
- LookupSwitch
- tableswitch
不同之处在于,lookupswitch使用带有键和标签的表格 ,而tableswitch只使用带有标签的表格 。
当执行一次tableswitch时 ,堆栈顶部的int值直接用作表中的索引,以便抓取跳转目标并立即执行跳转。 整个查询+跳转过程是一个O(1)操作 ,这意味着它的速度非常快。
执行lookupswitch时 ,将堆栈顶部的int值与表中的键进行比较,直到find匹配项,然后使用该键旁边的跳转目标执行跳转。 由于lookupswitch表总是必须sorting,因此keyX <keyY对于每个X <Y,整个查找+跳转过程是O(log n)操作,因为将使用二进制searchalgorithmsearch关键字(不需要进行比较对于所有可能的键find匹配或确定没有任何键匹配的int值)。 O(log n)比O(1)慢一些,但它仍然可以,因为许多众所周知的algorithm是O(log n),这些通常被认为是快速的; 即使O(n)或O(n * log n)仍然被认为是一个相当不错的algorithm(慢/坏algorithm有O(n ^ 2),O(n ^ 3)甚至更糟)。
编译器根据switch语句的紧凑性来决定使用哪个指令,例如
switch (inputValue) { case 1: // ... case 2: // ... case 3: // ... default: // ... }
上面的开关非常紧凑,没有数字“漏洞”。 编译器会像这样创build一个tableswitch:
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 }
这个代码很清楚这样的桌面切换是如何工作的。 val
是inputValue
, low
将为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
正如你所看到的,编译器必须为FakeTwoLabel
添加一个假的情况 。 由于2不是真正的开关值,所以FakeTwoLabel实际上是一个标签,它在默认情况下确切地改变了代码stream,因为值2实际上应该执行默认情况。
所以一个开关不一定非常紧凑,编译器才能创build一个表开关,但它至less应该非常紧凑。 现在考虑下面的开关:
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(log4)是2(这里的log是2的BTW的基数,而不是10的基数,因为计算机使用二进制数)。 这意味着VM最多需要两次比较才能findinputValue的标签,或者得出结论,该值不在表中,因此必须执行默认值。 即使表中有100个条目,最多需要7次比较才能find正确的标签或决定跳转到默认标签(7个比较比100个比较less得多,你不觉得吗?)。
所以这两条指令是可以互换的,或者两条指令的原因都有历史原因是无稽之谈。 对于两种不同types的情况,有两个指令,一个用于紧凑值(用于最大速度)的开关,另一个用于带有备用值的开关(不是所有数字孔都有最大速度,但仍然有很好的速度和非常紧凑的表格表示)。
javac 1.8.0_45编译到什么时候切换到哪一个?
要决定何时使用哪个,可以使用javac
selectalgorithm作为基础。
我们知道javac
的来源是在langtools
回购。
那我们grep:
hg grep -i tableswitch
第一个结果是langtools / src / share / classes / com / sun / tools / javac / jvm / Gen.java :
// 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;
哪里:
-
hi
:最大案例值 -
lo
:最小案例值
所以我们得出结论,它考虑到时间和空间的复杂性,时间复杂度为3 。
TODO我不明白为什么lookup_time_cost = nlabels
而不是log(nlabels)
,因为一个tableswitch
开关可以在二进制searchO(log(n))中完成。
奖金:C ++实现也在O(1)跳转表和O(long(n))二进制search之间进行类似的select: 切换if-else语句的优势
Java虚拟机规范描述了差异。 “当交换机的情况可以被有效地表示为目标偏移表中的索引时,使用tableswitch指令。 规范描述了更多细节。
由于Java字节码与下划线机器代码(如Sun自己的CPU)的某些特定绑定,我怀疑它大部分是历史的。
桌面切换本质上是一个计算跳转,从查找表中获取目的地。 相反,lookupswitch需要比较每个值,基本上迭代槽表元素直到find匹配值。
显然,这些操作码是可以互换的,但是根据数值,其中一个或另一个可能更快或更紧凑(例如,比较具有大间隙的一组按键和一组连续的按键)。