diff --git a/Documentations/4.2-gvn/README.md b/Documentations/4.2-gvn/README.md index c7863fc0cbe5f22cb73cc12261c609273f656694..e5435c915c237662d386f6920720a0104269dfe1 100644 --- a/Documentations/4.2-gvn/README.md +++ b/Documentations/4.2-gvn/README.md @@ -1,8 +1,11 @@ # Lab4 实验文档 - [Lab4 实验文档](#lab4-实验文档) + - [0. 前言](#0-前言) + - [1. GVN 基础知识](#1-gvn-基础知识) + - [1.1 GVN 简介](#11-gvn-简介) - [1.2 GVN 相关概念](#12-gvn-相关概念) - [1. IR 假设](#1-ir-假设) @@ -14,8 +17,11 @@ - [7. Join 操作](#7-join-操作) - [8. 变量等价与表达式等价](#8-变量等价与表达式等价) - [9. Transfer Function](#9-transfer-function) + - [2. GVN 算法(论文中提供的伪代码)](#2-gvn-算法论文中提供的伪代码) + - [3. 实验内容](#3-实验内容) + - [3.1 GVN pass 实现内容要求](#31-gvn-pass-实现内容要求) - [3.2 GVN 辅助类](#32-gvn-辅助类) - [3.3 注册及运行 GVN Pass](#33-注册及运行-gvn-pass) @@ -23,8 +29,11 @@ - [运行 Pass](#运行-pass) - [3.3 自动测试](#33-自动测试) - [3.4 Tips](#34-tips) + - [4. 提交要求](#4-提交要求) + - [目录结构](#目录结构) + - [提交要求和评分标准](#提交要求和评分标准) ## 0. 前言 diff --git a/Reports/4.2-gvn/figures/index_set.png b/Reports/4.2-gvn/figures/index_set.png new file mode 100644 index 0000000000000000000000000000000000000000..3d9385a602c77f4e8eaa85051e474928e4c1ebf6 Binary files /dev/null and b/Reports/4.2-gvn/figures/index_set.png differ diff --git a/Reports/4.2-gvn/figures/loop3.png b/Reports/4.2-gvn/figures/loop3.png new file mode 100644 index 0000000000000000000000000000000000000000..e8934a4084078aedf572d34d818bf960518fb722 Binary files /dev/null and b/Reports/4.2-gvn/figures/loop3.png differ diff --git a/Reports/4.2-gvn/figures/value_phi_func.png b/Reports/4.2-gvn/figures/value_phi_func.png new file mode 100644 index 0000000000000000000000000000000000000000..ab28725e763cc680952757e598bb31a54e63f490 Binary files /dev/null and b/Reports/4.2-gvn/figures/value_phi_func.png differ diff --git a/Reports/4.2-gvn/report.md b/Reports/4.2-gvn/report.md index 971bfa99ad283da14a91d2dd0bda12bff45ec727..1484c1f50c25da461773ef9666b83f86a6aeaf5d 100644 --- a/Reports/4.2-gvn/report.md +++ b/Reports/4.2-gvn/report.md @@ -66,6 +66,60 @@ detectEquivalences(G) 改完这些,终于能跑通第一版本了…… +### 对于函数`Intersect(Ci, Cj )` + +伪代码中,一个等价类是一个集合,求交后,v~k~是自然而然的在或不在新集合中,但是实现中并不这么简单,如何对这样的两个结构体进行逻辑上的取交: + +```cpp +struct CongruenceClass { + size_t index_; + Value *leader_; + std::shared_ptr value_expr_; + std::shared_ptr value_phi_; + std::set members_; +} +``` + +- 首先是Intersect的伪代码描述中,下边这句怎么在实现中体现? + + > C~k~ does not have value number + + 解决:index不同的自然就交不出v~i~,相同的时候才有v~i~即value number. + + 隐患:transferFunction要对v~i~有继承性,即不能每次涉及一个等价类都新开一个value number(这个在后边的值编号维护中已经解决) + +- 根据上一条讨论,得出一个观点:在伪代码中,一个等价类集合会出现的内容,在实现中的对应如下: + + - 普通变量:`members_` + + - 值表达式:`value_expr_` + + - phi函数:`value_phi_` + + - 值编号v~i~:`index` + + 因此,伪代码中对集合的求交,对应到实现上,就是对以上四个域求交。 + +- 额外设计:赋予**精准的**值编号 + + 在我的设计中,每条指令都有一个潜在的值编号,如果要为这条指令新建等价类,就使用这个值编号。所以需要`Intersect`确定新的值编号时,应该选用靠前的以为后边指令腾出编号空间。 + +### 对于函数`valuePhiFunc(ve, P)` + +- 输入就一个分区,实际上要用到两个前驱的分区,怎么处理? + + 传入基本块做参数。 + +- 二元运算的顺序颠倒怎么办? + + > 如phi(x1,y1)+phi(x2,y2)和phi(x1,y1)+phi(y2,x2) + > + > 对于第二种,单纯寻找x1+y2和y1+x2肯定不合逻辑 + + 论文中有详细的版本: + + ![](figures/value_phi_func.png) + ### 等价类设计 > ```cpp @@ -123,6 +177,23 @@ detectEquivalences(G) - 其他产生赋值的指令,也会由`transferFunction`传递给`ValueExpr`,这些指令是:`load`、`alloca`和返回值非空的`call`。 这里设计一种值表达式类型:`UniqueExpression`,这个表达式和任何其他都不等价,表现为`equiv`直接返回false。 + + > 看到有同学在的表达式打印都是`add v1 v2`这种,而我是`add %op1=... %op2=...`,好整洁好漂亮,心动了,就搞他。 + > + > 因为等价类树的叶子节点就两个:`constant`和这个`Unique` + > + > 常量打印对应的数字即可,而我想实现Unique打印v~i~的效果,所以在其中保存了等价类的值编号`index`,看起来值编号是等价类的属性,似乎不应该和表达式掺和,但是正如其名:UNIQUE,不应该与其他相同的,所以他一定是单独一个等价类,不受其他影响,index是恒定的。 + > + > 改好后,打印有如下效果: + > + > ``` + > [INFO] (GVN.cpp:820L run) + > index: 9 + > leader: %op13 = call i32 @input() + > value phi: nullptr + > value expr: v9 + > members: {%op13 = call i32 @input(); } + > ``` #### 若干需要梳理的问题 @@ -143,8 +214,14 @@ detectEquivalences(G) 这个问题源于`transferFunction`的一句话: > if ve or vpf is in a class Ci in POUTs + + 可以的,等价类中的元素都有唯一的表达式,其他等价类即使形式相同,操作数也不会相同,所以等价类和表达式其实是一一对应的关系。 + + (这也解释了UNIQUE中传入index的合理性) - 代表元如何选取?什么时候选取? + + 目前只对常量传播的Constant特别设置了主元,负责设置为`members_`的第一个元素。 - 怎么判断等价类相等 @@ -152,12 +229,6 @@ detectEquivalences(G) 从这个例子中可以发现,迭代收敛时,是pout中对应分区的`members_`不变,值编号还是会变化的,所以判断等价类相等,应该比较`members_`集合。 -- 什么时候给新的值编号? - - 1. 执行转移函数发现没有可以归属的等价类时 - - 2. 求交巴拉巴拉还没搞清楚那里 - - 等价类为空的判定,可以用`members_`做判断吗? 我认为是可以的,存在情况:两个基本块汇合时,交出一个集合,它的members为空,但是ve不空: @@ -180,88 +251,84 @@ detectEquivalences(G) 因为最终反映到IR上,我们直接关心的都是`members_`中的成员,所以用`members_`的空代表等价类的空我觉得是合适的。 -| 类型 | 处理 | -| ---------------- | ------------------------- | -| 二元运算 | 依伪代码 | -| 没有返回值的 | 不处理 | -| phi函数 | 本块中的不管,后继块的做考虑 | -| 函数调用 | 有返回值的考虑等价类,**注意后边纯函数的处理** | -| 产生指针(GEP、alloca) | 先为返回值单独新建等价类 | -| 类型转换及0扩展 | 单独新建等价类 | -| 比较 | 根据op和操作数递归判断,应该和二元运算类似 | -| load | 单独新建等价类 | +### 值编号的维护 -### 对于函数`Intersect(Ci, Cj )` +#### 值编号的目的 -伪代码中,一个等价类是一个集合,求交后,v~k~是自然而然的在或不在新集合中,但是实现中并不这么简单,如何对这样的两个结构体进行逻辑上的取交: +> 编号即`index_`不能一直递增,`loop3.ll`检测了这一点。 +> ![](figures/loop3.png) +> 关注`%op12`和基本块`label17`: +> +> 第二次迭代过程中,`label17`的两个前驱作交集,对应的是两个分区分别是: +> +> - `label11`的pout,第二次迭代结果 +> +> - `label24`的pout,第一次迭代结果 +> +> 如果每次新建等价类都使index递增,注意第二次经过`label11`新建出来的等价类编号和第一次也即`label24`那个不一样,这时候麻烦就来了:`INTERSECT`函数会交出一个`phi`函数。 +> +> 关键在于:index不应该递增,对于`%op12`,第二次迭代明明是同一个位置的定值,应该享有相同的值编号。 + +一个目标清晰起来:我要保证,不同迭代中,同一位置生成的等价类要享有相同的值编号,以标示同一个等价类。这里的同一个不是指完全相同,举例如下: -```cpp -struct CongruenceClass { - size_t index_; - Value *leader_; - std::shared_ptr value_expr_; - std::shared_ptr value_phi_; - std::set members_; -} +``` +// partition: {} +x=1 +// partition: {v1, 1, x} +y = 1 +// partition: {v1, 1, x, y} ``` -- 首先是Intersect的伪代码描述中,下边这句怎么在实现中体现? - - > C~k~ does not have value number - - 解决:index不同的自然就交不出v~i~,相同的时候才有v~i~即value number. - - 隐患:transferFunction要对v~i~有继承性,即不能每次涉及一个等价类都新开一个value number +后两个分区具有同一值编号`v1`,但是内部包含了不同的元素。 -- 根据上一条讨论,得出一个观点:在伪代码中,一个等价类集合会出现的内容,在实现中的对应如下: - - - 普通变量:`members_` - - - 值表达式:`value_expr_` - - - phi函数:`value_phi_` - - - 值编号v~i~:`index` - - 因此,伪代码中对集合的求交,对应到实现上,就是对以上四个域求交。 +所以值编号的作用是指示同一个等价类,而不表示相等。 -### 对于函数`valuePhiFunc(ve, P)` +#### 我的设计 -- 输入就一个分区,实际上要用到两个前驱的分区,怎么处理? - - 传入基本块做参数。 +> 助教有提示过: +> ![](figures/index_set.png) +> +> 我尝试了第2种,因为这个最简单,但是发现不可行:对loop3,第一次迭代进入`label0`,编号从3开始记,但是第二次迭代就要从5开始,因为`label_Entry`和`label28`相交,会拆分出`%op1`和`%op7`,而我还是index递增的逻辑,所以两个分别占用3、4,导致`label0`的编号从5开始,这样一来,在第一次迭代中编号为3的`%op8`无论如何也分不到编号3了。 -- 二元运算的顺序颠倒怎么办? - - > 如phi(x1,y1)+phi(x2,y2)和phi(x1,y1)+phi(y2,x2) - > - > 对于第二种,单纯寻找x1+y2和y1+x2肯定不合逻辑 +上边不成立是有一个本质的原因的。比如说`op1`,第一次迭代和`%op2`、`%op3`在一个等价类中,第二次被拆出来占用了一个新的编号(假设是3),而`%op8`想嗔怪`%op1`占用了编号3,本来就不占理,因为`%op8`在最开始就应该从一个更大的编号开始使用,为前边的指令预留出充足的编号空间(数据流收敛的过程是等价类拆分的过程)。 + +我灵机一动,想出了下面这个维护方案: + +- 首先,我遍历所有的指令是存在一个顺序的,我记录下这个信息 + +- 其次,等价类至多,也就是一条指令一个等价类 + +- 设计方案:每个指令的潜在值编号,是其遍历顺序。 - 应该不会有这种情况:phi表达式,追根溯源是在join时产生的: + 对一条指令执行转移函数时,如果有可以归属的等价类,就插入之, - ```cpp - GVN::join(const partitions &P1, const partitions &P2); - ``` - - 这里的分区是严格按照`Basicblock`的方法`get_pre_basic_blocks()`的返回顺序传入的,所以不会有担心的情况发生。 + 否则新建等价类的编号设置为遍历顺序。 -### 所见非所得 +这个方案可以保证一条指令执行后,如果新建了等价类,会产生相同的编号,以和后边的数据流标示同一个等价类,这样的编号空间预留可以避免编号冲突的问题。 -两方面: +实现上要注意两点: -- 论文中伪代码对于基本块的关注不是很多,但是我们的实现一定要围绕基本块进行,这个的解决对策说到了,其实和单条语句没有本质差异。 +- 每次调用针对指令的转移函数之前,都要重新设置值编号 -- 我们针对lightir优化,经常深入API中陷入到实现细节,还容易被C++的语言特性绊住,但是应该尝试从直观上理解:我们究竟要实现什么样的效果。这个就得去看ll文件找灵感。然后这还不够,回过头来看代码又呆住了,因为ll语句和那些类型对应不上,这个就是容易忽视的一点:看lightir类型的print函数,这个能帮我们直观地串联起实现细节和表象的ll文件。 +- `Intersect(Ci, Cj)`中可能发现两个等价类不是相同的值编号,这时要新建phi等价类,并赋予一个精准的值编号:两个前驱中的定值,分别对应一个值编号,选择遍历顺序靠前的那个。 -## 实验设计 +这个的实现就`Intersect`比较讲究技巧了,其他写起来都挺直观的。 + +### 常量传播的实现 -### join_helper +### 所见非所得 + +- 论文中伪代码对于基本块的关注不是很多,但是我们的实现一定要围绕基本块进行,这个的解决对策说到了,其实和单条语句没有本质差异。 -### _TOP +- 我们针对lightir优化,经常深入API中陷入到实现细节,还容易被C++的语言特性绊住,但是应该尝试从直观上理解:我们究竟要实现什么样的效果。这个就得去看ll文件找灵感。然后这还不够,回过头来看代码又呆住了,因为ll语句和那些类型对应不上,这个就是容易忽视的一点:看lightir类型的print函数,这个能帮我们直观地串联起实现细节和表象的ll文件。 -### transferFunction(Basicblock) +- 到了实验的后半阶段,调试信息的解读就变得越来越重要了。感谢某位同学劝我不要在用gdb debug,而是改用vscode,用gdb确实太慢了。 + + 这里调试的困难其实不在于调试的技巧,更多是信息解读,很多时候,用了半个小时甚至一上午才明白错误的原因在哪里,这也来源于对算法的理解不足。 + +## 实验设计 -### +> 设计见上述难点的描述 实现思路,相应代码,优化前后的IR对比(举一个例子)并辅以简单说明 diff --git a/gvn.json b/gvn.json new file mode 100644 index 0000000000000000000000000000000000000000..fcd0e05bd31ef65aff71f3939470672b4fafc43c --- /dev/null +++ b/gvn.json @@ -0,0 +1,3 @@ +[{ +"function": "main", +"pout": {"label_entry": [["%op0", ], ["%op1", ], ["%op2", "%op3", ], ],}},] \ No newline at end of file diff --git a/src/optimization/GVN.cpp b/src/optimization/GVN.cpp index d53b95ff05d93f19c452524641138ff7de8d1ff0..10e6eca8e1e22ccc876f843a3f8d3f75ee0284e5 100644 --- a/src/optimization/GVN.cpp +++ b/src/optimization/GVN.cpp @@ -263,9 +263,7 @@ GVN::intersect(shared_ptr ci, shared_ptr cj) { if (c->members_.size()) // not empty { if (c->index_ == 0) { - // it must be a phi instruction - // and be separated to 2 copy statement - // we should use the copy-stmt int the previous block + // we should use the `index` int the previous block auto instr = static_cast(*c->members_.begin()); auto instr_phi = dynamic_cast(instr); int exact_idx; @@ -570,7 +568,6 @@ GVN::transferFunction(Instruction *instr, Value *e, partitions pin) { } // TODO: get different ValueExpr by Instruction::OpID, modify pout - // ?? // get ve and vpf shared_ptr ve; if (e) {