[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同理指定0xf23de放入;遇到a+b时需要为a+b这整个指令计算一个hash值,比如可以定义+为1,然后hash(a+b) = hash(a)+1+hash(b),算法取决于实现。现在a+b的hash值为0xe52ba;当遇到第三行代码d=a+b时,编译器查表发现a+b已经计算过了,可以直接使用计算过的值,而不需要再次计算a+b;最后e也是查表发现b的存在而复用。

值编号的好处最明显的就是公共子表达式的消除,比如上面例子的a+b,其它还有常量替换,如果hash(a)发现hash表里面是常量,那么后面对a的使用可以直接替换为1。以及代数恒等式的消除。

前面说了值编号,那么C1使用的全局值编号(Global value numbering,GVN)是在多个基本块里面进行值编号,这样可以扩大优化范围,比如基本块A里面有a+b,隔着很远的基本块F里面也有a+b,GVN就可以消除该公共子表达式,要注意的坑是全局值编号的"全局"表示一个方法内的多个基本块,而不是编程语言里通常说的跨越方法的全局。与之相对的还有局部值编号(Local value numbering,LVN),它是指在一个基本块里面发现优化时机,这一步发生在C1编译器构建原始HIR的过程中。

2. C1编译器的全局值编号

HotSpot的全局值编号优化位于hotspot\share\c1\c1_ValueMap.cpp,它除了完成本职工作外还顺带做了短循环优化和循环不变代码外提。使用虚拟机标志-XX:+UseGlobalValueNumbering可开启GVN(默认开启),另外如果虚拟机是fastdebug版本,还可以加上-XX:+PrintValueNumbering -XX:+PrintLIRWithAssembly -XX:+PrintIR查看C1编译器内部GVN的详细流程。

3. 示例:公共子表达式消除(成功)

package com.github.kelthuzadx;

public class C1Optimizations {
    public static int gvn(int invariant, int num){
        int adder = invariant+8;
        while (num<100){
            num=invariant+8;
        }
        return num+adder;
    }

    public static void main(String[] args) {
         gvn(10,1024);
    }
}

gvn()函数里面invariant+8出现了两次,这样的公共表达式正是GVN大展身手的好地方,先关闭GVN(-XX:-UseGlovalValueNumbering)看看机器代码:

  mov    %eax,-0x9000(%rsp)
  push   %rbp
  sub    $0x30,%rsp                   

  mov    %rdx,%rax          ; adder=invariant
  add    $0x8,%eax          ; adder+=8
  jmpq   _Loop
  nop
_Loop
  mov    %rdx,%rsi          ; tmp = invariant
  add    $0x8,%esi          ; tmp+=8
  add    %r8d,%esi          ; tmp+=num
  mov    0x120(%r15),%r10   ; 安全点
  test   %eax,(%r10)        ; 轮询
  mov    %rsi,%r8           ; num = tmp
  cmp    $0x64,%r8d         ; if num<100
  jl     _Loop                                                                 

  add    %eax,%r8d
  mov    %r8,%rax
  add    $0x30,%rsp
  pop    %rbp
  mov    0x120(%r15),%r10
  test   %eax,(%r10)
  retq       

公共子表达式没有消除,循环里面创建了临时变量tmp并重复计算invariant+8。然后开启GVN(-XX:-UseGlobalValueNumbering):

  mov    %eax,-0x9000(%rsp)
  push   %rbp
  sub    $0x30,%rsp                   

  mov    %rdx,%rax        ; adder=invariant
  add    $0x8,%eax        ; adder+=8
  jmpq   _Loop
  nop

_Loop:
  add    %eax,%r8d        ; num+=adder
  mov    0x120(%r15),%r10 ; 安全点
  test   %eax,(%r10)      ; 轮询
  cmp    $0x64,%r8d       ; if num<100
  jl     _Loop           

  add    %eax,%r8d        ; num+=adder
  mov    %r8,%rax         ; ret_value = num
  add    $0x30,%rsp
  pop    %rbp
  mov    0x120(%r15),%r10
  test   %eax,(%r10)
  retq   

循环中检测到invariant+8是公共子表达式,已经计算过值,所以直接复用num+=adder,在给广播电视小

4. 示例:代数恒等式变换(失败)

还是之前的例子,我们增加一些数学恒等式:

public static int gvn(int invariant, int num){
    int adder = invariant+8;
    while (num<100){
        num+=invariant+8;
        num*=1;
        num/=1;
        num+=0;
        num-=0;
    }
    return num+adder;
}

HotSpot的GVN没有进行代数恒等式的变换,无论是否开启GVN都会产出对应的代码:

_Loop
  add    %edi,%r8d
  shl    $0x0,%r8d
  mov    %r8,%rax
  mov    $0x1,%ebx
  cmp    $0x80000000,%eax
  jne    0x000002ee006b9226
  xor    %edx,%edx
  cmp    $0xffffffff,%ebx
  je     0x000002ee006b9229
  cltd
  idiv   %ebx
  mov    0x120(%r15),%r10
  test   %eax,(%r10)
  mov    %rax,%r8
  cmp    $0x64,%r8d
  jl     _Loop             

相比之下g++ 8.0clang++ 8.0-O1优化强度上消除了多余的恒等式:

// g++ 8.0
gvn(int, int):
        lea     eax, [rdi+8]
        cmp     esi, 99
        jg      .L2
.L3:
        add     esi, eax
        cmp     esi, 99
        jle     .L3
.L2:
        add     eax, esi
        ret
// clang++ 8.0
gvn(int, int):                               # @gvn(int, int)
        mov     eax, esi
        mov     ecx, -8
        sub     ecx, edi
        add     edi, 8
.LBB0_1:                                # =>This Inner Loop Header: Depth=1
        add     eax, edi
        lea     edx, [rcx + rax]
        cmp     edx, 100
        jl      .LBB0_1
        ret

所以写Java的时候遇到恒等式(很少情况)如果可以请手动消除。

5. 循环不变代码外提(成功但受限)

循环不变代码外提(Loop Invariant Code Motion)很好理解,如果循环内某个值不会发生改变,那么不必每次都做计算,可以提到循环外面。但是循环不变代码外提优化有个严重的问题,它仅在关闭分层编译模式(-XX:-TieredCompilation)下才能进行。。。

public static int loopInvariantCodeMotion(int invariant, int num){
    for(int i=0;i<invariant*8+10;i++){
        num+=i;
    }
    return num;
}

关闭分层编译得到产出如下:

  mov    %eax,-0x9000(%rsp)
  push   %rbp
  sub    $0x30,%rsp                     

  mov    %rdx,%rax    ; tmp = invariant
  shl    $0x3,%eax    ; tmp*=8;
  add    $0xa,%eax    ; tmp+= 10
  mov    $0x0,%esi    ; i=0
  jmpq   _Cond
  nop

_Loop
  add    %esi,%r8d    ; num+=i
  inc    %esi         ; i++
  mov    0x120(%r15),%r10  ;安全点
  test   %eax,(%r10)       ;轮询
_Cond:
  cmp    %eax,%esi    ; if i<tmp
  jl     _Loop             

  mov    %r8,%rax
  add    $0x30,%rsp
  pop    %rbp
  mov    0x120(%r15),%r10
  test   %eax,(%r10)
  retq   

如果可能,请尽量将循环不变代码手动外提,而不是(盲目)依赖JIT编译器。

6. 其它

我们不能简单的根据某个指标来评判事物好坏,看到C++做了某种优化Java没做就批评Java,这样不好也是不公平的,与其口舌之争不如深入分析为什么后者没有做某种优化。虚拟机的编译是JIT,动态编译器的编译成本是需要计算在运行成本之内的,它的每个优化都需要经过深思熟虑,很可能JIT的profiling-guided优化一项就抵消C++数个优化得到的收益叠加;反之亦然。

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

时间: 2024-07-30 00:07:53

[Inside HotSpot] C1编译器优化:全局值编号(GVN)的相关文章

[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],

[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_

编译器优化陷阱——全局指针多次使用异常

做程序开发一定会和编译器打交道,编译器优化可以给我们代码运行带来一定的提升,但也可能存在一些意想不到的问题.下面就是我在开发时候遇到的一个坑,希望可以给大家一些借鉴 直接上代码说话吧 1 static unsigned char* s_data = NULL; //存储一帧视频数据 2 void DoRendering () 3 { 4 // D3D11 case 5 if (s_DeviceType == kUnityGfxRendererD3D11 && EnsureD3D11Reso

C1编译器的实现

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

C语言 之编译器优化

C语言的编译器会对变量和代码进行一定的优化,我们看下面这个例子. int a,b,c; a=1; b=a; c=b; 这个程序正常运行的时候会这样子:先把a指向的内存空间内放入1 再把a指向的内存空间里的数读出来放到b指向的内存空间, 最后再把b指向的内存空间里的数读出来放到c指向的内存空间里. 然而编译器要对这个程序进行一定的优化,编译的时候,直接把1放入三个内存空间中. 这样子在正常运行的时候确实是好事,但是总会有一些特殊的情况发生,比如,一个中断程序突然改变了a的值,那么会发生这样的情况:

对String类型的认识以及编译器优化

Java中String不是基本类型,但是有些时候和基本类型差不多,如String b = "tao" ; 可以对变量直接赋值,而不用 new 一个对象(当然也可以用 new). Java中的变量和基本类型的值存放于栈内存,而new出来的对象本身存放于堆内存,指向对象的引用还是存放在栈内存.例如如下的代码: int  i=1; String s =  new  String( "Hello World" ); 变量i和s以及1存放在栈内存,而s指向的对象"H

C#编译器优化那点事

使用C#编写程序,给最终用户的程序,是需要使用release配置的,而release配置和debug配置,有一个关键区别,就是release的编译器优化默认是启用的. 优化代码开关即optimize开关,和debug开关一起,有以下几种组合. | 编译器开关设置 |C#IL代码质量 | JIT本地代码质量 | | ------------- |:-------------:| -----:| | /optimize- /debug-(默认) | 未优化 | 有优化 | | /optimize-

深入理解JVM读书笔记四: (早期)编译器优化

10.1概述 Java 语言的 "编译期" 其实是一段 "不确定" 的操作过程,因为它可能是指一个前端编译器(其实叫 "编译器的前端" 更准确一些)把 .java 文件转变成 .class 文件的过程:也可能是指虚拟机的后端运行期编译器(JIT 编译器,Just In Time Compiler)把字节码转变成机器码的过程:还可能是指使用静态提前编译器(AOT 编译器,Ahead Of Time Compiler)直接把 *.java 文件编译成

一个函数返回临时对象引起的编译器优化问题

我们都知道,如果在一个函数调用另一个函数,假设是 main 函数调用 fun 函数,这个 fun 函数返回一个临时类类型变量,那么这个时候编译器就会在 main 函数申请一个空间并生成一个临时对象,通过拷贝构造函数将 fun 返回的临时变量的值拷贝到这个临时对象.我们看如下的代码: #include <iostream> #include <cstring> using namespace std; class Matrix { public: explicit Matrix(do