编译器优化记录(Mem2Reg+SSA Destruction)
编译器优化记录(2) Mem2Reg+SSA Destruction
写的时候忽然想起来,这部分的内容恰好是在我十八岁生日的前一天完成的。算是自己给自己的一份成长的纪念吧。
0. 哪些东西可以Mem2Reg
顾名思义,Mem2Reg的意思是我们可以通过维护每个函数中局部变量被赋值之后产生的副本来消除对其alloca,而后进行一系列load/store的过程(众所周知这一类操作是需要更多时间的)。
一般来说,所有的alloca都可以被消除,但是对于某个函数超过\(8\)个参数而言,情况会不太一样。具体而言,它们是从栈上传过来的,因而我们需要将其load到一个新的寄存器上。
另外,对于函数的\(0\to7\)号参数,在进行完Mem2Reg之后也有后续操作。我们需要在函数刚开始的时候把这几个参数从物理寄存器\(a_0,...,a_7\)转移到到我们给他们分配的虚拟寄存器上。
1. 确定插入phi的位置
在[上一份博客](编译器优化记录(控制流图,支配树) - Radioheading - 博客园 (cnblogs.com))中,我们已经获得了插入phi的前置内容(即支配树、支配边界),那么接下来我们就可以插入phi指令。
注意,下面的内容都是以函数为单位进行的,Mem2Reg的本质应该是一个Function Pass
根据上文所述的规则,我们可以找到每个可以被消除的局部变量,只需要遍历enterBlock中的各个alloca。接下来,我们对于每个可以消除的局部变量\(alloca\),记录它在每个块中的最后一次\(def\),这样的目的很明显,是为了记录在支配边界,该变量应当被赋值为什么。
我们采用HashMap<IRRegister, HashMap<BasicBlock, entity>> all来记录对于每个局部变量,它在每个块中的最后一次定值。它肯定会发生改变,因为我们在某个块中插入phi指令的时候,就又创造了一次定值。
然后,我们采用工作表算法来解决。
工作表算法(WorkList Algorithm)就是说,我们用一个集合\(W\)来作为工作表,每次选取其中的一个节点,进行一个操作,然后加入这个操作会影响的其他节点(假使它们不在其中),直到工作表为空。我们大部分的优化都会用到它。
我们使用工作表\(W\),它的初始值为所有对\(alloca\)进行赋值(即store)的集合。我们同时用HashSet<BasicBlock> F来记录所有已经出现过关于alloca的接下来,我们选取其中的一个块bb1,如果!all.get(alloca).containsKey(bb1),那就说明bb1中对这个变量的最后一个定值一定是phi指令,我们要对all进行更新。接下来,我们就需要枚举bb1的支配边界来插入phi指令了。对于它的支配边界中的一个块Y,如果!F.contains(Y),那么我们就需要插入phi指令啦。同时我们也要把Y分别加入F和W中。
我对于phi指令的设计是这样的:
public class IRPhi extends IRBaseInst {
public HashMap<BasicBlock, entity> block_value = new HashMap<>();
public IRRegister dest;
public IRRegister origin;
}
同时,我在BasicBlock中加入了HashMap<IRRegister, IRPhi> phiMap来记录一个块中的所有phi,在执行toString()的时候优先输出。
经过这个过程,我们可以获得所有需要插入phi的地方。接下来,就是考虑从每条路径来的时候,这个变量应该被赋值为什么了。
2. 变量重命名
一个直观的想法是这样的。对于每个局部变量\(alloca\)和它出现的某个块,如果这个块中,在这条指令上面有对它的定值,那么就直接使用这个定值定出来的东西。如果啥定值都没有,那么这肯定说明连phi都没有,说明从控制流的角度来说,上一次对它定值一定是在它的直接支配节点或更上面。
那么,我们为了寻找这个“最后一次定值”,就需要在支配树上进行DFS。
接下来,我们考虑对于每个个块的操作,也即visitBlock(BasicBlock block, Function func)。
2.1 需要使用的数据结构
我们使用HashMap<IRRegister, entity> last_def来记录当前每个变量最后\(def\)使用的值。例如%add = add i32 %0, 1; store i32 %add, ptr @s就可以被理解为,当前对变量s的最后一次\(def\)使用的是%add。
我们使用HashMap<IRRegister, entity cur_name>来表示我们需要修改的虚拟寄存器。例如在上面两条指令之后,%1 = load i32, ptr @s; %add1 = add i32 %1, 1中的%1就可以被替代为%add。
需要注意的是,在进入到同一个块的不同支配树后继时,last_def, cur_name都应该维持一致。这就需要我们每个块内给它们开一个副本,在一个后继访问完后,把它们的副本重新赋回来,然后再访问下一个后继。
2.2 操作流程
首先自然是开副本,注意java的引用赋值特性。
接下来,我们考察每个基本块的所有指令(这里指令包含所有的phi)。如果这条指令是一个store或者一个phi,那么我们就修改last_def。如果这条指令是一个load,那么我们就修改cur_name。
然后我们调整后继节点中phi的使用值。伪代码如下
for (block的每个后继succ) {
for (succ 的每一个phi) {
记element为这个phi原本的局部变量
if (last_def.get(element) != null) {
phi加入(block, last_def.get(element))的这个entry
}
}
}
最后,我们访问block的每一个支配后继,并把副本赋回来,并删掉和我们消除的这些局部变量有关的load/store/alloca。
2.3 加入默认分支
考虑这样一个情况,有基本块\(BB_1, BB_2, BB_3\)满足\(pred(BB_3) = \{BB_1, BB_2\}, pred(BB_1)=pred(BB_2)=enterBlock\)。如果我们一开始定义了一个局部变量\(x\),并只在\(BB_1\)中对其定值,且在\(BB_3\)中使用之。那么根据上面的操作,\(BB_3\)中关于\(x\)的phi指令只有一个源头。然而,如果你尝试用clang编译它,会报一长串的错误。这是因为对于\(BB_3\)的前驱还包括\(BB_2\)。而规范应该是对于每一个前驱,都有一个赋值。于是我们需要对那些没出现的前驱补充值,这里姑且赋成初始值吧(i32, i8赋值为0,ptr赋值为null)。
进一步思考,这其实算是对于源代码的语义精化。换言之,在这里我们可能改变了源代码的意义(尽管它可能是不安全的)。
3. SSA Destruction
注意到,刚刚的插入phi的过程仍然保持了Single Static Assignment的性质。但是在之后指令选择(指令选择(instruction selection)是将中间语言转换成汇编或机器代码的过程。在LLVM后端中具体表现为模式匹配)的阶段,我们并没有对phi指令的对应翻译方法。那就需要我们在IR过渡到汇编的过程中把phi转化成多次分别的赋值,这显然会打破每个虚拟寄存器只能被定值一次的准则。
一个可以想见的转化方法如下:
enter_main_0:
br label %for.cond_0
for.cond_0:
%i_phi_0 = phi i32 [ %inc_0, %for.inc_0 ], [ 0, %enter_main_0 ]
%x_phi_0 = phi i32 [ %add_0, %for.inc_0 ], [ 1, %enter_main_0 ]
%slt_0 = icmp slt i32 %i_phi_0, 10
br i1 %slt_0, label %for.body_0, label %for.end_0
for.inc_0:
%inc_0 = add i32 %i_phi_0, 1
br label %for.cond_0
for.body_0:
%add_0 = add i32 %x_phi_0, 1
br label %for.inc_0
for.end_0:
br label %exit_main_0
exit_main_0:
ret i32 %x_phi_0
}
enter_main_0:
%i_phi_tmp_0 = 0
%x_phi_tmp_0 = 1
br label %for.cond_0
for.cond_0:
%x_phi_0 = %x_phi_tmp_0
%i_phi_0 = %i_phi_tmp_0
%slt_0 = icmp slt i32 %i_phi_0, 10
br i1 %slt_0, label %for.body_0, label %for.end_0
for.inc_0:
%inc_0 = add i32 %i_phi_0, 1
%i_phi_tmp_0 = %inc_0
%x_phi_tmp_0 = %add_0
br label %for.cond_0
for.body_0:
%add_0 = add i32 %x_phi_0, 1
br label %for.inc_0
for.end_0:
br label %exit_main_0
exit_main_0:
ret i32 %x_phi_0
}
考虑%phi = phi i32 [0, bb1], [1, bb2]我们就在bb1/bb2的末端插入一个对%phi的赋值。这里我用了一个并不存在的llvm-ir指令IRMove,并在指令选择阶段直接将其变成了Move。
当然,如果你仔细看了上面的这段代码,你会发现我是先把所有值赋给一个%tmp,再在出现phi的那个基本块中将其赋值给%phi。这是为什么呢?
我们可以回顾支配边界的定义。可以想象,存在一种控制流图使得某个节点的支配边界有它自己。那这就会导致上面的方法对这两种代码会执行不同的结果:
BB1:
%i_phi_0 = phi i32 [ %x_phi_0, %for.inc_0 ], [ 0, %enter_main_0 ]
%x_phi_0 = phi i32 [ %i_phi_0, %for.inc_0 ], [ 1, %enter_main_0 ]
...
BB1:
%x_phi_0 = phi i32 [ %i_phi_0, %for.inc_0 ], [ 1, %enter_main_0 ]
%i_phi_0 = phi i32 [ %x_phi_0, %for.inc_0 ], [ 0, %enter_main_0 ]
...
这两个phi变量互相赋值,这样它们的先后顺序会影响它们在`%for.inc_0`这个块中的赋值结果。为了解决之,我们采用了新增虚拟寄存器的策略。这某种程度上和时序逻辑有些相似。毕竟每个块中的所有phi都应该是严格在同一时间完成的。
p.s.如果你读过编译器指导手册的话,你会发现,我们的这个操作也省掉了增加空块以避免数据竞争的操作。
修改完上述内容之后,你的Mem2Reg应该就能重新通过asm的所有测试点了。
4. 参考资料
[1] [SSA book](CnTransGroup/StaticSingleAssignmentBookChinese: 《Static Single Assignment Book》- 中文翻译 (github.com))
[2] 编译器指导手册(预览8.1)
编译器优化记录(Mem2Reg+SSA Destruction)的更多相关文章
- C#编译器优化那点事 c# 如果一个对象的值为null,那么它调用扩展方法时为甚么不报错 webAPI 控制器(Controller)太多怎么办? .NET MVC项目设置包含Areas中的页面为默认启动页 (五)Net Core使用静态文件 学习ASP.NET Core Razor 编程系列八——并发处理
C#编译器优化那点事 使用C#编写程序,给最终用户的程序,是需要使用release配置的,而release配置和debug配置,有一个关键区别,就是release的编译器优化默认是启用的.优化代码 ...
- 探索c#之尾递归编译器优化
阅读目录: 递归运用 尾递归优化 编译器优化 递归运用 一个函数直接或间接的调用自身,这个函数即可叫做递归函数. 递归主要功能是把问题转换成较小规模的子问题,以子问题的解去逐渐逼近最终结果. 递归最重 ...
- VS编译器优化诱发一个的Bug
VS编译器优化诱发一个的Bug Bug的背景 我正在把某个C++下的驱动程序移植到C下,前几天发生了一个比较诡异的问题. 驱动程序有一个bug,但是这个bug只能 Win32 Release 版本下的 ...
- VS2010/2012配置优化记录笔记
VS2010/2012配置优化记录笔记 在某些情况下VS2010/2012运行真的实在是太卡了,有什么办法可以提高速度吗?下面介绍几个优化策略,感兴趣的朋友可以参考下,希望可以帮助到你 有的时候V ...
- 翻译「C++ Rvalue References Explained」C++右值引用详解 Part6:Move语义和编译器优化
本文为第六部分,目录请参阅概述部分:http://www.cnblogs.com/harrywong/p/cpp-rvalue-references-explained-introduction.ht ...
- Visual C++中的编译器优化
博客搬到了fresky.github.io - Dawei XU,请各位看官挪步.最新的一篇是:Visual C++中的编译器优化.
- gcc编译器优化给我们带来的麻烦???
gcc编译器优化给我们带来的麻烦??? 今天看到一个很有趣的程序,如下: ? 1 2 3 4 5 6 7 8 9 int main() { const int a = 1; int * ...
- C#编译器优化那点事
使用C#编写程序,给最终用户的程序,是需要使用release配置的,而release配置和debug配置,有一个关键区别,就是release的编译器优化默认是启用的. 优化代码开关即optimize开 ...
- React性能优化记录(不定期更新)
React性能优化记录(不定期更新) 1. 使用PureComponent代替Component 在新建组件的时候需要继承Component会用到以下代码 import React,{Componen ...
- 【转】C 编译器优化过程中的 Bug
C 编译器优化过程中的 Bug 一个朋友向我指出一个最近他们发现的 GCC 编译器优化过程(加上 -O3 选项)里的 bug,导致他们的产品出现非常诡异的行为.这使我想起以前见过的一个 GCC bug ...
随机推荐
- 驱动开发:内核LoadLibrary实现DLL注入
远程线程注入是最常用的一种注入技术,在应用层注入是通过CreateRemoteThread这个函数实现的,该函数通过创建线程并调用 LoadLibrary 动态载入指定的DLL来实现注入,而在内核层同 ...
- 推送服务接入指导(HarmonyOS篇)
消息推送作为App运营日常使用的用户促活和召回手段,是与用户建立持续互动和连接的良好方式.推送服务(Push Kit)是华为提供的消息推送平台,建立了从云端到终端的消息推送通道,本文旨在介绍Harmo ...
- DevOps|中式土味OKR与绩效考核落地与实践
昨天一个小伙伴和我讨论了一下OKR和绩效管理,所以这次想简单明了地说下在中国怎么做比较合适,很多高大上的理论无法落地也是空中楼阁. 首先说一些,我个人的理解 道德品质和能力素质决定了一个人的职位行为 ...
- 适合Windows桌面、Material Design设计风格、WPF美观控件库【强烈推荐】
推荐一个在Github已start超过13.6K,非常流行.美观的WPF控件库. 项目简介 这是一个适用于Windows桌面,全面且易于使用的控件库,遵循Google推测的Material Desig ...
- 理解ffmpeg
ffmpeg是一个完整的.跨平台的音频和视频录制.转换和流媒体解决方案. 它的官网:https://ffmpeg.org/ 这里有一份中文的文档:https://ffmpeg.p2hp.com/ ff ...
- 【项目学习】ERC-4337 抽象账户项目审计过程中需要注意的安全问题
抽象账户是什么 抽象账户(也有叫合约钱包)是 EIP-4337 提案提出的一个标准.简单来说就是通过智能合约来实现一个"账户(account)",在合约中自行实现签名验证的逻辑.这 ...
- 关于 Task 简单梳理
〇.前言 Task 是微软在 .Net 4.0 时代推出来的,也是微软极力推荐的一种多线程的处理方式. 在 Task 之前有一个高效多线程操作累 ThreadPool,虽然线程池相对于 Thread, ...
- 最为常用的Laravel操作(2)-路由
基本路由 // 接收一个 URI 和一个闭包 Route::get('hello', function () { return 'Hello, Laravel'; }); // 支持的路由方法 Rou ...
- altas2.1.0编译、安装、集成CDH6.3.2
目录 altas2.1.0编译.安装.集成CDH6.3.2 一: Atlas源码下载 二: Atlas源码编译 1.修改altas项目主pom文件,即需要编译的CDH6.3.2对应版本信息 2.Atl ...
- ChatGPT 1.0.0安卓分析,仅限国内分享
ChatGPT 1.0.0安卓分析,仅限国内分享 博客园首发,本文将对ChatGpt Android版本1.0.0 APK进行静态解包分析和抓包分析,从ChatGpt Android APK功能的设计 ...