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. C1编译器中的条件表达式消除

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

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

3. CEE示例

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

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代码是条件表达式。它认为的条件表达式是三元表达式,有些出乎意料,但是也请继续吧:

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辅证(也可使用 -XX:+PrintCEE),产它出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]

根据右边助记指令还是很好理解: 如果i4<i5depend<10则返回1,反之返回0。接着查看条件表达式消除之后的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,最后返回。这里的模式正是条件传送指令的工作机制,HIR也有明显的改变,经过一番折腾,我们成功的让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编译器优化:条件表达式消除的更多相关文章

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

    1. 值编号 我们知道C1内部使用的是一种图结构的HIR,它由基本块构成一个图,然后每个基本块里面是SSA形式的指令,关于这点如可以参考[Inside HotSpot] C1编译器工作流程及中间表示. ...

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

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

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

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

  4. JavaScrip条件表达式优化

    目录 1,前言 2,多条件if语句优化 3,参数默认值 4,Switch语句优化 1,前言 今早看了一篇文章<JavaScrip实现:如何写出漂亮的条件表达式>,原创于:华为云开发者社区, ...

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

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

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

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

  7. C1编译器的实现

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

  8. C#编译器优化那点事

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

  9. 【转】C 编译器优化过程中的 Bug

    C 编译器优化过程中的 Bug 一个朋友向我指出一个最近他们发现的 GCC 编译器优化过程(加上 -O3 选项)里的 bug,导致他们的产品出现非常诡异的行为.这使我想起以前见过的一个 GCC bug ...

随机推荐

  1. matplotlib简单的使用(二)

    1.折线图 import matplotlib as mlb from matplotlib import pylab as pl # 折线图 # 分别创建x,y坐标 x = [1,3,5,7,6,9 ...

  2. Hexo的更新 主题的更换

    1:HEXO更新 ①hexo generate ②hexo deploy 2:  HEXO主题的更换,①找到主题的github地址后 进入自己的HEXO文件夹然后 git clone xxxx(地址) ...

  3. 7. 整合shiro,搭建粗粒度权限管理

    shiro是一个易用的权限管理框架,只需提供一个Realm即可在项目中使用,本文就将结合上一篇中搭建的权限模块.角色模块和用户模块来搭建一个粗粒度的权限管理系统,具体如下:1. 添加shiro依赖和与 ...

  4. Python跨目录调程序

    #!/usr/bin/python # -*- coding: utf-8 -*- # 导入其它目录下的文件, 需要去帮获取当前程序的绝对路径并加入到环境变量的相对路径中 import os impo ...

  5. 测试APPEND INSERT是否产生UNDO信息的过程

    D:\>sqlplus test/testSQL*Plus: Release 11.1.0.6.0 - Production on 星期三 06月 29 19:46:41 2016Copyrig ...

  6. C++中使用引用作为函数参数的优点

    1.传递引用给函数与传递指针的效果是一样的.这时,被调函数的形参就成为原来主调函数中的实参变量或对象的一个别名来使用,所以在被调函数中对形参变量的操作就是对其相应的目标 对象(在主调函数中)的操作. ...

  7. Hadoop问题:DataNode进程不见了

      DataNode进程不见了 问题描述 最近配置Hadoop的时候出现了这么一个现象,启动之后,使用jps命令之后是这样的: 看不到DataNode进程,但是能够正常的工作,是不是很神奇啊? 在一番 ...

  8. BZOJ_2882_工艺_后缀数组

    BZOJ_2882_工艺_后缀数组 Description 小敏和小燕是一对好朋友. 他们正在玩一种神奇的游戏,叫Minecraft. 他们现在要做一个由方块构成的长条工艺品.但是方块现在是乱的,而且 ...

  9. 哎呀,我老大写Bug啦——记一次MessageQueue的优化

    MessageQueue,顾名思义消息队列,在系统开发中也是用的比较多的一个中间件吧.我们这里主要用它来做日志管理和订单管理的,记得老老大(恩,是的,就是老老大,因为他已经跳槽了)还在的时候,当时也是 ...

  10. Discuz3.4-SSRF-从触发点到构造payload

    目录 SSRF逆向分析 0x00 前言 0x01 收集情报 0x02 尝试逆向找到触发点 0x03 尝试构造payload 0x04 总结 SSRF逆向分析 0x00 前言 之前有复现过一些漏洞,但是 ...