编译器优化记录(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 ...
随机推荐
- 读少写多的条件下 ConcurrentHashMap 和 ReadWriteLock 的选择
场景是这样的:两个对象往一个 Map 里循环写入,另外一个对象偶尔读一次,写的频率比读的频率高很多.希望实现的是读的时候暂停写入.CocurrentHashMap 和 ReadWriteLock 各有 ...
- in用不用索引,啥时候能用啥时候不能用,一文说清
in/or到底能不能用索引应该是肯定的,但有时生效有时不生效,这个能不能量化计算?这是本文想讨论和解答的问题. in到底用不用索引感觉像一桩悬疑片!古早时期的面经,统一说不走索引,在一些程序员脑海中从 ...
- ChatGPT小型平替之ChatGLM-6B本地化部署、接入本地知识库体验
本文期望通过本地化部署一个基于LLM模型的应用,能让大家对构建一个完整的应用有一个基本认知.包括基本的软硬环境依赖.底层的LLM模型.中间的基础框架及最上层的展示组件,最终能达到在本地零编码体验的目的 ...
- 聊聊 ASP.NET 6 整洁架构开发模板
大家好,我是Edison. 最近看了一些整洁架构(CleanArchitecture)的文章,自己和同事也简单写了一个基于整洁架构的ASP.NET 6开发模板在玩.这里就仅仅抛个砖,案例主要以自己根据 ...
- php发起支付加密数据供获取订单状态使用
<?php//作者主页: https://www.bz80.comnamespace Index\Controller; class PayController extends BaseCont ...
- 文盘Rust -- FFI 浅尝
rust FFI 是rust与其他语言互调的桥梁,通过FFI rust 可以有效继承 C 语言的历史资产.本期通过几个例子来聊聊rust与C 语言交互的具体步骤. 场景一 调用C代码 创建工程 car ...
- 【C#/.NET】RESTful风格的Post请求与CreateAtAction
目录 引言 实现步骤 概念介绍 创建控制器 总结 引言 在构建Web应用程序时,遵循RESTful风格的API设计原则能够使我们的系统更加灵活.可扩展和易于维护.其中,Post请求在创建资源时起 ...
- Unity的IActiveBuildTargetChanged:深入解析与实用案例
Unity IActiveBuildTargetChanged Unity IActiveBuildTargetChanged是Unity引擎中的一个非常有用的功能,它可以让开发者在切换构建平台时自定 ...
- Python安装time库失败?不是吧阿sir你还不知道内置模块不用下载吧
嗨嗨,今天给python安装time库,一直报错,换源等办法都试过了 直到我看到 Python中有以下常用模块不用单独安装 random模块 sys模块 time模块 os系统操作 re正则操作 js ...
- 普通用户启动 supervisor 报 HTTP 错误(strace)
公司的开发对生产环境都有普通用户 www 的权限,采用堡垒机登录到生产环境的机器. 默认 supervisor 使用 root 用户启动,开发没有权限直接修改配置和操作 supervisor 管理的进 ...