[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指令。

原文地址:https://www.cnblogs.com/kelthuzadx/p/10759438.html

时间: 2024-08-27 15:29:05

[Inside HotSpot] C1编译器优化:条件表达式消除的相关文章

[Inside HotSpot] C1编译器优化:全局值编号(GVN)

1. 值编号 我们知道C1内部使用的是一种图结构的HIR,它由基本块构成一个图,然后每个基本块里面是SSA形式的指令,关于这点如可以参考[Inside HotSpot] C1编译器工作流程及中间表示.值编号(Value numbering)是指为每个计算得到的值分配一个独一无二的编号,然后遍历指令寻找可优化的机会.比如下面的代码: a = 1;b=4; c = a+b; d = a+b; e = b; 编译器可以在计算a的时候为它指定一个hash值(0x12a3e)然后放入hash表:b同理指定

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

1. C1编译器线程 C1编译器(aka Client Compiler)的代码位于hotspot\share\c1.C1编译线程(C1 CompilerThread)会阻塞在任务队列,当发现队列有编译任务即可CompileTask的时候,线程唤醒然后调用CompilerBroker,CompilerBroker再进一步选择合适编译器,以此进入JIT编译器的世界. CompilerBroker到C1编译器进行JIT编译的调用栈如下: CompileBroker::invoke_compiler_

C编译器剖析_4.2 语义检查_表达式的语义检查(7)_二元运算符_赋值运算_条件表达式

在前文对语义检查进行简介时,我们已初步介绍过用于对二元运算符表达式进行语义检查的函数CheckBinaryExpression,为了阅读方便,这里我们再次给出图4.2.2.在本小节中,我们准备对第1126至1144行中的各个函数进行讨论. 图4.2.2 CheckBinaryExpression() 对于形如a+b的二元运算表达式,我们要通过在前面章节中介绍的函数CommonRealType来求得整个表达式a+b的类型,如果a为int型且b为double型,则表达式a+b的类型为double.在

C1编译器的实现

总览 词法.语法分析 分析方案 词法 语法 符号表 类型系统 AST 语义检查 EIR代码生成器 MIPS代码生成器 寄存器分配 体系结构相关特性优化 使用说明 编译 运行 总览 C1语言编译器及流程 C1 语言是一个类 C 的语言.语言的特征为: 包含 int.float 和 bool 简单类型以及以这些类型为基本类型的多维数组类型. 一个 C1 程序包含多个函数.全局变量声明和常量声明,其中必须有一个 void main(void)主函数. 函数可以带参数,也可以不带参数,参数的类型是简单类

编译器,优化,及目标代码生成.

本文介绍从源文件开始到目标代码生成的过程. 首先,是我们每天都要接触的源文件.源文件是由纯ASCII或者其他字符集组成的文本,由程序员使用文本编辑器创建.它有以下的几种形式 纯文本.好处是易于维护.并且可以使用处理文本文件的程序来处理源文件. 这个就是我们最常见的源代码形式了.甚至可以使用notepad来处理源文件! 记号化的源文件.使用专门的单字节"记号"值来表示源文件中的保留字等语句元素. 好处1:尺寸小,由于使用单字节的符号来"压缩"多字符的保留字,所以比纯文

编译错误 error C2451: “std::_Unforced”类型的条件表达式是非法的

part 1 编译器 vs2015 VC++. 完整的错误信息粘贴如下: d:\program files (x86)\microsoft visual studio 14.0\vc\include\algorithm(43): error C2451: “std::_Unforced”类型的条件表达式是非法的 完整代码: struct Bigger { bool operator() (vector<string>::size_type lhs, vector<string>::

【C语言探索之旅】 第一部分第六课:条件表达式

内容简介 1.课程大纲 2.第一部分第六课:条件表达式 3.第一部分第七课预告:循环语句 课程大纲 我们的课程分为四大部分,每一个部分结束后都会有练习题,并会公布答案.还会带大家用C语言编写三个游戏. C语言编程基础知识 什么是编程? 工欲善其事,必先利其器 你的第一个程序 变量的世界 运算那点事 条件表达式 循环语句 实战:第一个C语言小游戏 函数 练习题 习作:完善第一个C语言小游戏 C语言高级技术 模块化编程 进击的指针,C语言王牌 数组 字符串 预处理 创建你自己的变量类型 文件读写 动

条件表达式的短路求值与函数的延迟求值

延迟求值是 .NET的一个很重要的特性,在LISP语言,这个特性是依靠宏来完成的,在C,C++,可以通过函数指针来完成,而在.NET,它是靠委托来完成的.如果不明白什么是延迟求值的同学,我们先看看下面的一段代码: static void TestDelayFunction() { TestDelayFunton1(true,trueFun3); } static void TestDelayFunton1(bool flag , Func<bool> fun ) { if(flag) fun(

C语言优化实例:消除多级指针的间接访问

如果一个多层次的数据结构达到两级或者两级以上,举例如下: struct A{ int array_member[100]; //其他数据成员 }; struct B{ struct A *a_ptr; //其他数据成员 } 那么通过B类型的指针b_ptr访问A类型的array_member的某一个元素array_member[0]则需要使用b_ptr->a_ptr->array_member[0]这种多级指针的形式.如果一个函数中多次用到这个变量的话,可以采用一个临时变量保存这个多级指针:in