[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.0和clang++ 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)的更多相关文章
- [Inside HotSpot] C1编译器优化:条件表达式消除
1. 条件传送指令 日常编程中有很多根据某个条件对变量赋不同值这样的模式,比如: int cmov(int num) { int result = 10; if(num<10){ result ...
- [Inside HotSpot] C1编译器工作流程及中间表示
1. C1编译器线程 C1编译器(aka Client Compiler)的代码位于hotspot\share\c1.C1编译线程(C1 CompilerThread)会阻塞在任务队列,当发现队列有编 ...
- [Inside HotSpot] C1编译器HIR的构造
1. 简介 这篇文章可以说是Christian Wimmer硕士论文Linear Scan Register Allocation for the Java HotSpot™ Client Compi ...
- 翻译「C++ Rvalue References Explained」C++右值引用详解 Part6:Move语义和编译器优化
本文为第六部分,目录请参阅概述部分:http://www.cnblogs.com/harrywong/p/cpp-rvalue-references-explained-introduction.ht ...
- C#编译器优化那点事 c# 如果一个对象的值为null,那么它调用扩展方法时为甚么不报错 webAPI 控制器(Controller)太多怎么办? .NET MVC项目设置包含Areas中的页面为默认启动页 (五)Net Core使用静态文件 学习ASP.NET Core Razor 编程系列八——并发处理
C#编译器优化那点事 使用C#编写程序,给最终用户的程序,是需要使用release配置的,而release配置和debug配置,有一个关键区别,就是release的编译器优化默认是启用的.优化代码 ...
- java编译器优化和运行期优化
概述 最近在看jvm优化,总结一下学习的相关知识 (一)javac编译器 编译过程 1.解析与填充符号表过程 1).词法.语法分析 词法分析将源代码的字符流转变为标记集合,单个字符是程序编 ...
- [Inside HotSpot] Java的方法调用
1. 方法调用模块入口 Java所有的方法调用都会经过JavaCalls模块.该模块又细分为call_virtual调用虚函数,call_static调用静态函数等.虚函数调用会根据对象类型进行方法决 ...
- C1编译器的实现
总览 词法.语法分析 分析方案 词法 语法 符号表 类型系统 AST 语义检查 EIR代码生成器 MIPS代码生成器 寄存器分配 体系结构相关特性优化 使用说明 编译 运行 总览 C1语言编译器及流程 ...
- gcc编译器优化给我们带来的麻烦???
gcc编译器优化给我们带来的麻烦??? 今天看到一个很有趣的程序,如下: ? 1 2 3 4 5 6 7 8 9 int main() { const int a = 1; int * ...
随机推荐
- 基于分支限界法的旅行商问题(TSP)二
和上篇一样,考前写写伪代码,考完了补上具体的解释和代码. 状态{矩阵,结果集,下界} 全局结果集列表,全局上界初始为Infinite 建立一个heap,存储状态,出堆规则为拥有最小的下界. 利用red ...
- 关于DataSet中Relations的应用
using System; using System.Collections.Generic; using System.Text; using System.Data; namespace conn ...
- ubuntu 命令整合1
一.linux命令基本格式 命令名[选项] [参数] 注意: 命令名区分字母大小写 命令名必须有 选项.参数可以没有,选项一般使用减号开头二.具体Linux命令 1.who 显示登录系统中的用户的信息 ...
- linux使用windows磁盘,挂载共享目录
实例说明:客户两台服务器,一台web服务器(linux)只有50G,课程资源太多太大导致磁盘不够用:客户的文档服务器(windows)磁盘很大超过1T,所以产生了,将web资源使用文档服务器磁盘的想法 ...
- TensorFlow源码安装
前言 TensorFlow如果能二进制包安装,我真的不想选择自己编译,但是情况不由人,好不容易找到一台服务器,CPU不支持AVX指令集,安装的release版本运行到import tensorflow ...
- Java Web之九九乘法表
NineTabs.jsp 1 <%@ page language="java" import="java.util.*" contentType=&quo ...
- pycharm跨目录调用文件
笔者今天在一个测试qq群中遇到解决跨目录中调用文件的问题.现在来详细说一下. 以下图目录为例: 1.同目录下直接调用掉用.test2.py和test3.py在同一个目录test2下. 所以在test3 ...
- LeetCode Javascript实现 258. Add Digits 104. Maximum Depth of Binary Tree 226. Invert Binary Tree
258. Add Digits Digit root 数根问题 /** * @param {number} num * @return {number} */ var addDigits = func ...
- 系列博文-Three.js入门指南(张雯莉)-网格 setInterval方法 requestAnimationFrame方法 使用stat.js记录FPS
第6章 动画 在本章之前,所有画面都是静止的,本章将介绍如果使用Three.js进行动态画面的渲染.此外,将会介绍一个Three.js作者写的另外一个库,用来观测每秒帧数(FPS). CSS3动画那么 ...
- app后端设计(10)--数据增量更新
在新浪微博的app中,从别的页面进入主页,在没有网络的情况下,首页中的已经收到的微博还是能显示的,这显然是把相关的数据存储在app本地. 使用数据的app本地存储,能减少网络的流量,同时极大提高了用户 ...