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编译器。

最后想多说一点,我们不能简单的根据某个指标来评判事物好坏,看到C++做了某种优化Java没做就批评Java,这样不好也是不公平的,与其口舌之争不如深入分析为什么后者没有做某种优化。虚拟机的编译是JIT,动态编译器的编译成本是需要计算在运行成本之内的,它的每个优化都需要经过深思熟虑。

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

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

    1. 条件传送指令 日常编程中有很多根据某个条件对变量赋不同值这样的模式,比如: int cmov(int num) { int result = 10; if(num<10){ result ...

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

    1. C1编译器线程 C1编译器(aka Client Compiler)的代码位于hotspot\share\c1.C1编译线程(C1 CompilerThread)会阻塞在任务队列,当发现队列有编 ...

  3. [Inside HotSpot] C1编译器HIR的构造

    1. 简介 这篇文章可以说是Christian Wimmer硕士论文Linear Scan Register Allocation for the Java HotSpot™ Client Compi ...

  4. 翻译「C++ Rvalue References Explained」C++右值引用详解 Part6:Move语义和编译器优化

    本文为第六部分,目录请参阅概述部分:http://www.cnblogs.com/harrywong/p/cpp-rvalue-references-explained-introduction.ht ...

  5. C#编译器优化那点事 c# 如果一个对象的值为null,那么它调用扩展方法时为甚么不报错 webAPI 控制器(Controller)太多怎么办? .NET MVC项目设置包含Areas中的页面为默认启动页 (五)Net Core使用静态文件 学习ASP.NET Core Razor 编程系列八——并发处理

    C#编译器优化那点事   使用C#编写程序,给最终用户的程序,是需要使用release配置的,而release配置和debug配置,有一个关键区别,就是release的编译器优化默认是启用的.优化代码 ...

  6. java编译器优化和运行期优化

    概述    最近在看jvm优化,总结一下学习的相关知识 (一)javac编译器 编译过程 1.解析与填充符号表过程 1).词法.语法分析    词法分析将源代码的字符流转变为标记集合,单个字符是程序编 ...

  7. [Inside HotSpot] Java的方法调用

    1. 方法调用模块入口 Java所有的方法调用都会经过JavaCalls模块.该模块又细分为call_virtual调用虚函数,call_static调用静态函数等.虚函数调用会根据对象类型进行方法决 ...

  8. C1编译器的实现

    总览 词法.语法分析 分析方案 词法 语法 符号表 类型系统 AST 语义检查 EIR代码生成器 MIPS代码生成器 寄存器分配 体系结构相关特性优化 使用说明 编译 运行 总览 C1语言编译器及流程 ...

  9. gcc编译器优化给我们带来的麻烦???

    gcc编译器优化给我们带来的麻烦??? 今天看到一个很有趣的程序,如下: ? 1 2 3 4 5 6 7 8 9 int main() {     const int a = 1;     int * ...

随机推荐

  1. MySQL的变量分类总结

    在MySQL中,my.cnf是参数文件(Option Files),类似于ORACLE数据库中的spfile.pfile参数文件,照理说,参数文件my.cnf中的都是系统参数(这种称呼比较符合思维习惯 ...

  2. Java学习导航

    由于最近在系统的重新学习Java,为了便于日后复习,给个人博客中Java内容做一个目录. Java基础:Java虚拟机(JVM) Java基础:内存模型 Java基础:JVM垃圾回收算法 Java基础 ...

  3. 如何识别企业内的“千里马”?

    很多企业主都在感叹无法找到合适的优秀管理人才,却忽视了一条获得管理人才的捷径:内部培养.在员工中挑选具有领导潜质的人才,加以培养,必成企业栋梁,而且这样选拔的人才与企业的契合度.忠诚度方面都相对较高. ...

  4. OpenStack架构详解

    OpenStack提供开放源码软件,建立公共和私有云. OpenStack是一个社区和一个项目,以及开放源码软件,以帮助企业运行的虚拟计算或者存储云. OpenStackd开源项目由社区维护,包括Op ...

  5. MySQL 各类日志文件介绍

    日志文件 1.错误日志 ErrorLog 错误日志记录了MyQLServer运行过程中所有较为严重的警告和错误信息,以及MySQLServer每次启动和关闭的详细信息. 在默认情况下,系统记录错误日志 ...

  6. SSM-SpringMVC-30:SpringMVC中InitBinder的骇客级优化

     ------------吾亦无他,唯手熟尔,谦卑若愚,好学若饥------------- 上篇博客利用initbinder做了局部的日期类型转换,但是兼容性不要,只支持yyyy-MM-dd这种,所以 ...

  7. TGI

    淘宝的很多分析会用到TGI这个指标,但是是如何计算的以及该如何理解和应用这个指标?TGI:即Target Group Index(目标群体指数)TGI指数= [目标群体中具有某一特征的群体所占比例/总 ...

  8. Base64 image

    [前端攻略]:玩转图片Base64编码 什么是 base64 编码? 我不是来讲概念的,直接切入正题,图片的 base64 编码就是可以将一副图片数据编码成一串字符串,使用该字符串代替图像地址. 这样 ...

  9. Using variables inside Postman and Collection Runner

    Variables are among the most powerful features in Postman. Using variables in your Postman requests, ...

  10. linux 安装 PHP fileinfo 扩展

    将windows解压Linux服务器 1.错误: PHP Fileinfo extension must be installed/enabled to use Intervention Image. ...