[Inside HotSpot] C1编译器优化:条件表达式消除

Posted kelthuzadx

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[Inside HotSpot] C1编译器优化:条件表达式消除相关的知识,希望对你有一定的参考价值。

1. 条件传送指令

日常编程中有很多根据某个条件对变量赋不同值这样的模式,比如:

int cmov(int num) {
    int result = 10;
    if(num<10){
        result = 1;
    }else{
        result = 0;
    }
    return result;
}

如果不进行编译优化会产出cmp-jump组合,即根据cmp比较的结果进行跳转。可以使用gcc -O0查看:

cmov(int):
        push    rbp
        mov     rbp, rsp
        mov     DWORD PTR [rbp-20], edi
        int result = 10;
        mov     DWORD PTR [rbp-4], 10
        cmp     DWORD PTR [rbp-20], 9
        jg      .L2
        mov     DWORD PTR [rbp-4], 1
        jmp     .L3
.L2:
        mov     DWORD PTR [rbp-4], 0
.L3:
        mov     eax, DWORD PTR [rbp-4]
        pop     rbp
        ret

这里如果num>9就为result赋1,否则赋0。正因为跳转是条件的,CPU必须要等到条件成立才执行后面的指令(即数据依赖于条件),这会让处理器变慢,所以CPU通常会使用分支预测算法,提前执行某个概率更高的分支,即使这个时候条件没有成立。但问题是,这是预测不是断言,如果它预测条件成立进入A分支给R赋值V1,实际却是条件失败,应该继续B分支给R赋V2,那么CPU还需要额外撤销给R赋值V1。而条件传送系列指令(cmovcc,setcc)则不同,它根据EFLAGS的位条件性的选择给R赋V1还是V2,这样就没有分支预测失败的性能惩罚。回到上面的例子,使用gcc8的-O3可以看到产出的条件传送指令:

cmov(int):
        xor     eax, eax
        cmp     edi, 9
        setle   al
        ret

产出变短了,code cache能容纳下更多代码不说,这里setle会根据ZF结果,即小于等于9则给al赋1,否则赋0。这里我们又侧面证明了C++有条件表达式消除,那么HotSpot有吗?还真有,但是别高兴的太早...

2. HotSpot VM C1编译器中的条件表达式消除

[Inside HotSpot] C1编译器工作流程及中间表示中我们提到C1编译器将字节码转化为机器码的过程分为多个阶段:

技术图片

第一个阶段它生成HIR,一种C1内部用到的表示字节码的图结构。这个阶段同时也会对它进行优化,其中就包括本文要讨论的条件表达式消除(Condition expressions elimination)。但是在研究之前我们还需要一些准备工作,HotSpot无法直接查看虚拟机JIT产出的汇编,需要下载hsdis-amd64.dll,将它放在jdk/bin/server/目录下,然后虚拟机加上参数-XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly。另外,为了确保虚拟机用C1编译方法,还要加上-Xcomp -XX:TieredStopAtLevel=1

3. CEE示例

准备好后我们可以开始尝试,准备一段和上面gcc差不多的代码:

package com.github.kelthuzadx;

public class C1Optimizations {
    public static int conditionalExpressionElimination(int depend){
        if(depend<10){
            return 1;
        }else{
            return 0;
        }
    }

    public static void main(String[] args) {
         conditionalExpressionElimination(1024);
    }
}

然后加上参数查看汇编形式的产出(实际上还是本地代码的,只是汇编形式输出而已):

; Simplified
com/github/kelthuzadx/C1Optimizations.conditionalExpressionElimination(I)I 
  0x000001afb0a691c0: mov    %eax,-0x9000(%rsp)
  0x000001afb0a691c7: push   %rbp
  0x000001afb0a691c8: sub    $0x30,%rsp                     ;*iload_0 

  0x000001afb0a691cc: cmp    $0xa,%edx

  0x000001afb0a691cf: jge    L1             ;*if_icmpge
  0x000001afb0a691d5: mov    $0x1,%eax
  0x000001afb0a691da: add    $0x30,%rsp
  0x000001afb0a691de: pop    %rbp
  0x000001afb0a691df: mov    0x120(%r15),%r10               ; 安全点
  0x000001afb0a691e6: test   %eax,(%r10)                    ; 轮询
  0x000001afb0a691e9: retq                                  ;*ireturn 

L1:  
  0x000001afb0a691ea: mov    $0x0,%eax
  0x000001afb0a691ef: add    $0x30,%rsp
  0x000001afb0a691f3: pop    %rbp
  0x000001afb0a691f4: mov    0x120(%r15),%r10               ; 安全点
  0x000001afb0a691fb: test   %eax,(%r10)                    ; 轮询
  0x000001afb0a691fe: retq   
  0x000001afb0a691ff: nop
  0x000001afb0a69200: nop

并没有优化?HotSpot不认为上面的Java代码是条件表达式(这是有原因的,可能也是个BUG,进一步交流请私信)。它认为的条件表达式是三元表达式(?:):

package com.github.kelthuzadx;

public class C1Optimizations {
    public static int conditionalExpressionElimination(int depend){
        return depend>10?1:0;
    }

    public static void main(String[] args) {
         conditionalExpressionElimination(1024);
    }
}

要确定是HotSpot认为这是条件表达式而不是我们认为这是条件表达式,得加上-XX:+PrintIR -XX:+PrintLIRWithAssembly参数,这样可以产出C1 HIR及其汇编表示。首先看看原始HIR:

IR after parsing
B4 [0, 0] -> B0 sux: B0
empty stack
inlining depth 0
__bci__use__tid____instr____________________________________
. 0    0     14    std entry B0

B0 (SV) [0, 3] -> B2 B1 sux: B2 B1 pred: B4
empty stack
inlining depth 0
__bci__use__tid____instr____________________________________
  1    0    i5     10
. 3    0     6     if i4 <= i5 then B2 else B1

B1 (V) [6, 7] -> B3 sux: B3 pred: B0
empty stack
inlining depth 0
__bci__use__tid____instr____________________________________
  6    0    i7     1
. 7    0     8     goto B3
                   stack [0:i7]

B3 (V) [11, 11] pred: B1 B2Stack:
0  i11 [ i7 i9] 

stack [0:i11]
inlining depth 0
__bci__use__tid____instr____________________________________
. 11   0    i12    ireturn i11

B2 (V) [10, 11] -> B3 sux: B3 pred: B0
empty stack
inlining depth 0
__bci__use__tid____instr____________________________________
  10   0    i9     0
. 11   0     10    goto B3
                   stack [0:i9]

根据右边助记指令还是很好理解:

if i4<i5:       // if(depend<10>)
 return i7      // return 1
else
 return i11     // return 0

接着查看条件表达式消除(CEE)之后的HIR:

IR after CEE
B4 [0, 0] -> B0 sux: B0
empty stack
inlining depth 0
__bci__use__tid____instr____________________________________
. 0    0     14    std entry B0

B0 (SV) [0, 3] -> B3 sux: B3 pred: B4
empty stack
inlining depth 0
__bci__use__tid____instr____________________________________
  1    0    i5     10
  3    0    i15    0
  3    0    i16    1
  3    0    i17    i4 <= i5 ? i15 : i16
. 3    0     18    goto B3
                   stack [0:i17]

B3 (V) [11, 11] pred: B0
stack [0:i17]
inlining depth 0
__bci__use__tid____instr____________________________________
. 11   0    i12    ireturn i17

i17的值视条件而定,如果条件i4<i5则i17为0,否则为1,最后返回。经过一番折腾,我们终于成功的让HotSpot对条件表达式做了一次消除。不过还没有结束,我们再看看HIR经过一系列步骤后最终生成的代码:

[Disassembling for mach='i386:x86-64']
   2 std_entry 
  0x0000026a64f209a0: mov    %eax,-0x9000(%rsp)
  0x0000026a64f209a7: push   %rbp
  0x0000026a64f209a8: sub    $0x30,%rsp

  0x0000026a64f209ac: cmp    $0xa,%edx

  0x0000026a64f209af: mov    $0x0,%eax
  0x0000026a64f209b4: jle    0x0000026a64f209bf
  0x0000026a64f209ba: mov    $0x1,%eax

  0x0000026a64f209bf: add    $0x30,%rsp
  0x0000026a64f209c3: pop    %rbp
  0x0000026a64f209c4: mov    0x120(%r15),%r10
  0x0000026a64f209cb: test   %eax,(%r10)
  0x0000026a64f209ce: retq   

很遗憾,即便是高层次上HIR做了条件表达式消除,后面低层次LIR到本地代码生成也没有产出cmov指令。

以上是关于[Inside HotSpot] C1编译器优化:条件表达式消除的主要内容,如果未能解决你的问题,请参考以下文章

[Inside HotSpot] C1编译器工作流程及中间表示

JIT即时编译器(C1和C2)

[inside hotspot] java方法调用的StubCode

执行引擎:程序编译与代码优化

Java Hotspot client模式和server模式的区别

JVM_3_程序编译与代码优化