diff --git a/Reports/.gitignore b/Reports/.gitignore index 8d999aa8ff8deabef1b7142967f89f860490299c..80da8ccf9070f75c23900850b3f030fc7014f38a 100644 --- a/Reports/.gitignore +++ b/Reports/.gitignore @@ -2,3 +2,4 @@ 2-ir-gen-warmup/*.pdf 3-ir-gen/*.pdf 4.1-ssa/*.pdf +4.2-gvn/*.pdf diff --git a/Reports/4.2-gvn/report.md b/Reports/4.2-gvn/report.md index 1484c1f50c25da461773ef9666b83f86a6aeaf5d..c651a4eb2296d930013dea1104d7408cc6dc963b 100644 --- a/Reports/4.2-gvn/report.md +++ b/Reports/4.2-gvn/report.md @@ -1,10 +1,12 @@ # Lab4.2 实验报告 -姓名 学号 +> 李晓奇 PB20111654 + +[TOC] ## 实验要求 -请按照自己的理解,写明本次实验需要做什么 +依照伪代码,填充框架实现GVN. ## 实验难点 @@ -34,37 +36,31 @@ detectEquivalences(G) - 怎么设计top? - 尽管算法伪代码中,top是对每一条语句的赋值的,但是任意一条语句的都有所属基本块,只要对基本块的pout标注top、就能保证基本块的pin合乎逻辑,逐条执行转移函数后,每条语句的pin都合法。`` + 尽管算法伪代码中,top是对每一条语句的赋值的,但是任意一条语句都有所属基本块,只要对基本块的pout标注top、就能保证基本块的pin合乎逻辑,逐条执行转移函数后,每条语句的pin都合法。 - `while changes to any POUT occur` 这句话如此轻松,但是在实现中出现了问题:这涉及到怎么判断分区相等,做不好会出现死循环。 - 如果单纯比较index恐怕不行:因为我目前的`transferFunction`会生成一个新的等价类,赋予一个新的编号。 - - 所以我使用对等价类中的`members`做相等的判断,如果两个分区中的等价类都能找到相等的,那么就判断分区相等。 - - > 但是这样依赖于set的排序, - > - > - 对于分区,这个排序依赖于等价类中index的值,侥幸来说每条指令按顺序执行,即使index不一致(每次转移函数分配最新的index),大小顺序也是一致的? - > - > - 对于等价类,里边是`Value*`指针,这个指针的等价就是语义上的等价,没问题。 + 分区中的同一个等价类,集合中元素也可能发生了增减,在这里的语义下是不相等的。所以我使用对等价类中的`members`做相等的判断,如果两个分区中的等价类都能找到相等的,那么就判断分区相等。 改过来后发现还是死循环,DEBUG很久意识到我的分区相等写的有问题:`return members_ == other.members_`,这样会挨个儿比较`members_`指针的值,而不是我预想的比较指针指向的等价类。所以要手动遍历一下。 这个遍历也不是很直观的,因为要做两层解引用: - - ```cpp - for (; it1 != p1.end(); ++it1, ++it2) - if (not(**it1 == **it2)) - return false; - ``` - + +```cpp + for (; it1 != p1.end(); ++it1, ++it2) + if (not(**it1 == **it2)) + return false; +``` + `*it`是等价类的指针,`**it`才是等价类。 - - 除此之外,我还忘了`++it2`,淦。 - - 改完这些,终于能跑通第一版本了…… + +> 这里不用逐对比较依赖于set的排序: +> +> 对于分区(等价类指针的set),小于号根据index重载,所以排序依赖于等价类中index的值。 +> +> 如果分区结果已经收敛,那么由于遍历顺序一致,会有每条分区中的等价类具有相同的index排布,比较时正确true。 ### 对于函数`Intersect(Ci, Cj )` @@ -102,13 +98,22 @@ struct CongruenceClass { - 额外设计:赋予**精准的**值编号 - 在我的设计中,每条指令都有一个潜在的值编号,如果要为这条指令新建等价类,就使用这个值编号。所以需要`Intersect`确定新的值编号时,应该选用靠前的以为后边指令腾出编号空间。 + 在我的[设计](#值编号的维护)中,每条指令都有一个潜在的值编号,如果要为这条指令新建等价类,就使用这个值编号。所以需要`Intersect`确定新的值编号时,应该选用靠前的以为后边指令腾出编号空间。(详见`Intersect`中的`exact_idx`变量) ### 对于函数`valuePhiFunc(ve, P)` - 输入就一个分区,实际上要用到两个前驱的分区,怎么处理? - 传入基本块做参数。 + 传入基本块做参数,函数声明改成: + + ```cpp + std::shared_ptr valuePhiFunc( + std::shared_ptr ve, + BasicBlock *bb, + Instruction *instr); + ``` + + bb是当前块,用来找到两个前驱,instr用来做常量折叠,只有其op会被用到 - 二元运算的顺序颠倒怎么办? @@ -116,11 +121,13 @@ struct CongruenceClass { > > 对于第二种,单纯寻找x1+y2和y1+x2肯定不合逻辑 - 论文中有详细的版本: + 吐槽这个论文离谱之时(省去诸多细节),突然发现论文其实有详细的版本: ![](figures/value_phi_func.png) + + 在这个基础上,我又加入了一些常量折叠的内容,见[后边的讨论](#常量传播的实现) -### 等价类设计 +### 等价类 > ```cpp > shared_ptr @@ -129,71 +136,115 @@ struct CongruenceClass { 等价类涉及到针对指令设计、常量折叠、递归计算等,很复杂,需要拎出来单独讨论。 -首先注意到传给传进去的参数是指令类型(指针)。需要`transferFunction`处理的指令,一次赋值的右式自然可以由指令类型得到,所以没有毛病。 +首先注意到传进去的参数是指令类型(指针)。需要`transferFunction`处理的指令,一次赋值的右式自然可以由指令类型得到,所以没有毛病。 + +指令有多种类型,也即lightIR中有多种赋值方式,而非单纯的伪代码中的二元运算。但是即使是已经在算法中讨论过的二元运算,写起来也不容易。在此之外,还有一些自己设计的等价类。 + +实验达到最后,所有等价类可以从`Expression::gvn_expr_t`中窥见: + +```cpp + enum gvn_expr_t { + e_constant, + e_bin, + e_cmp, + e_phi, + e_cast, + e_gep, + e_unique, + e_global, + e_argument, + e_call + }; +``` + +新增加的等价类及其含义如下: -指令有多种类型,也即lightIR中有多种赋值方式,而非单纯的伪代码中的二元运算。但是即使是已经在算法中讨论过的二元运算,写起来也不容易。 +| 等价类 | 含义 | +| --------------- | ------------------------ | +| cmp | 整形/浮点比较 | +| cast | 类型转换 | +| gep | GEP指令 | +| unique | 不做特殊处理但会用到的,如load、alloca | +| global、argument | 全局变量和形参 | +| call | 函数调用 | -#### 生成等价类的逻辑 +#### 等价类设计 -首先讨论生成等价类的逻辑: +首先讨论这些等价类设计,在实验中发现,这些设计主要是要保存足够的信息,能够支持我们做正确的相等判断,而这些信息又和lighit的设计/接口紧密关联: -- 二元运算`y op z` +- e_constant + + > 常量折叠会在后边详细探讨 - 如果y、z都是常量,那么可以进行常量折叠,得到**一个**`ConstantExpression`类型。 + 常量等价类,这个在常量折叠中起关键作用,其中保存一个`Constant*`变量。 + +- e_bin - 否则返回的是一个`BinaryExpression`类型,这个类型需要左右操作数,都是`Expression`类型,如果操作数是常量,使用`ConstantExpression`创建新的值表达式,否则递归调用`valueExpr`。 + 二元运算的构成:左右操作数+操作符。 + +- e_cmp - > 这里为什么递归调用不会走到未定义? + lightir中,比较指令的操作符并不是单一的,他是一个两层的信息:op+cmpop,所以设计中`CmpExpression`继承自`BinaryExpression`后再加入记录cmpop的信息。 -- phi函数 +- e_phi - 我们逻辑上把其改变成了copy,`transferFunction`应该维护这一点,所以认为没有phi函数传入。 + phi表达式的构成只需要两个操作数即可。 + +- e_cast - 具体是`transferFunction`函数中的这几句话: + 操作符是三层的信息:op+源类型+目的类型。 - ```cpp - GVN::partitions - GVN::transferFunction(Instruction *x, Value *e, partitions pin) - { - auto e_instr = dynamic_cast(e); - auto e_const = dynamic_cast(e); - assert((not e or e_instr or e_const) && - "A value must be from an instruction or constant"); - // …… - if (e) { - if (e_const) - ve = ConstantExpression::create(e_const); - else - ve = valueExpr(e_instr, &pin); - } else - ve = valueExpr(x, &pin); - } - ``` + 此外需要记录(源)操作数的expression。 -- 比较函数:和二元运算一致。 +- e_gep + + GEP表达式的结构:基地址(expression)+索引(vector\) -- 类型转换:添加新的表达式类型`CastExpression`,处理和二元操作类似。 +- e_unique + + 这个表达式是为了对应load和alloca,其左值都不需特别处理,但是又应该有表达式。 + + 使用一个`Instruction*`变量保存左值,做比较用。 + + 还加入了index,这个表达式是树的叶子节点,我希望它输出是v~index~这样。 -- 其他产生赋值的指令,也会由`transferFunction`传递给`ValueExpr`,这些指令是:`load`、`alloca`和返回值非空的`call`。 +- e_global和e_argument - 这里设计一种值表达式类型:`UniqueExpression`,这个表达式和任何其他都不等价,表现为`equiv`直接返回false。 + 全局变量、形参类型也要有归属的表达式,但是和上述的unique不兼容,所以各自新建等价类,直接保存对应的指针。这样分开也方便控制打印输出。 + +- e_call - > 看到有同学在的表达式打印都是`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(); } - > ``` + 函数调用算是最复杂的一个,涉及到纯函数分析,还有函数类型、实参比较等很多需要考虑的。在这个表达式中我记录了这些信息: + + - `pure_`:是否是纯函数 + + - `f_`:`Function*`类型,用来判断是否是同一个函数 + + - `args_`:实参的`expression`容器。 + + 再来看看比较的函数: + + ```cpp + bool equiv(const CallExpression *other) const { + // single instruction, should be same + if (instr_ == other->instr_) + return true; + if (not(pure_ and f_ == other->f_)) + return false; + for (int i = 0; i < args_.size(); i++) + if (not(*args_[i] == *other->args_[i])) + return false; + return true; + } + ``` + + 这里总有bug,最后的逻辑是: + + - 如果是同一个调用(同一条指令),那么认为是相等的 + + - 如果是同一个纯函数的等价调用,认为是相等的 + + - 否则不等 #### 若干需要梳理的问题 @@ -201,13 +252,13 @@ struct CongruenceClass { - 等价类中的ve一定存在吗? - 根据等价类的设计,ve一定是存在的,且等价类中`members_`中一定存在元素。 + 根据等价类的设计,ve一定是存在的,且等价类中`members_`中一定存在元素(如果删除某一个元素后,等价类为空,要连带着从分区删掉这个等价类) - ve一定唯一吗? - 从逻辑上,等价类集合中的元素享有共同的ve产生的结果,ve是唯一的 + 从逻辑上,等价类集合中的元素享有共同的ve,所以ve是唯一的 - 从实现上,添加 + 从实现上,添加元素,添加元素的ve和等价类的ve是相等的,有种归纳法的感觉,总之ve应该是唯一的。 - ve或者vpf能够作为代表吗? @@ -215,19 +266,21 @@ struct CongruenceClass { > if ve or vpf is in a class Ci in POUTs - 可以的,等价类中的元素都有唯一的表达式,其他等价类即使形式相同,操作数也不会相同,所以等价类和表达式其实是一一对应的关系。 + 可以的,等价类中的元素都有唯一的表达式,其他等价类即使形式相同,操作数也不会相同/等价,所以等价类和表达式其实是一一对应的关系。 (这也解释了UNIQUE中传入index的合理性) - 代表元如何选取?什么时候选取? - 目前只对常量传播的Constant特别设置了主元,负责设置为`members_`的第一个元素。 + 目前只对常量传播的Constant特别设置了主元, + + 否则设置为`members_`的第一个元素。 - 怎么判断等价类相等 - 根据论坛上这个[例子](http://cscourse.ustc.edu.cn/forum/thread.jsp?forum=91&thread=68),我发现值编号会有非必要的增长,即最终收敛时用到的编号是1、5、6,其中2、3、4都是迭代中用到的但是最后都没有体现。 + 在我的值编号还没有得到解决时(还在一直递增),值编号显然不可以拿来作比较,所以选择比较`members_`,直到最后。 - 从这个例子中可以发现,迭代收敛时,是pout中对应分区的`members_`不变,值编号还是会变化的,所以判断等价类相等,应该比较`members_`集合。 + 而在经过了值编号设计后,再来分析,发现的确不能比较值编号,即使值编号。这在[后边](#值编号的目的)有详细讨论。 - 等价类为空的判定,可以用`members_`做判断吗? @@ -247,9 +300,58 @@ struct CongruenceClass { BB3: ... ; preds = BB1, BB2 ``` - 进入BB3时,对BB1和BB2的pout中等价类分别取交,执行`Intersect(Ci={v3, x1, v1+v2}, Cj={v4, x2, v1+v2}`,得到的是`{v1+v2}`,`members_`中没有成员,但是ve存在,我认为这种情况也应该判定结果为空集。 + 进入BB3时,对BB1和BB2的pout中等价类分别取交, - 因为最终反映到IR上,我们直接关心的都是`members_`中的成员,所以用`members_`的空代表等价类的空我觉得是合适的。 + 执行`Intersect(Ci={v3, x1, v1+v2}, Cj={v4, x2, v1+v2}`, + + 得到的是`{v1+v2}`,注意`members_`中没有成员,但是ve存在,我认为这种情况也应该判定结果为空集。 + + `Intersect`函数中以`members`非空作后续生成phifunc的前提条件,也是来源于此。 + +### copy statement + +对于IR中的phi指令(非phi函数),我们逻辑上将phi函数转换成了copy statement,这在实现中体现在`transferFunction(x, e, pin)`的调用上:当copy statement发生时,传入的x是一个phi指令,而e是等式的右值,相关调用: + +```cpp +// in transferFunction(BB) +if (instr.is_phi()) { + if ((res = pretend_copy_stmt(&instr, bb)) == -1) + continue; + part = transferFunction(&instr, instr.get_operand(res), part); +} +``` + +其中`pretend_copy_stmt(instr, bb)`负责找到instr(phi指令)中的哪个操作数是来自基本块bb的,如果没有就返回-1。 + +`transferFunction(x, e, pin)`中对应的处理(如何生成相关的等价类): + +```cpp +GVN::partitions +GVN::transferFunction(Instruction *x, Value *e, partitions pin) +{ + auto e_instr = dynamic_cast(e); + auto e_const = dynamic_cast(e); + // auto e_global = dynamic_cast(e); + auto e_argument = dynamic_cast(e); + assert((not e or e_instr or e_const or e_argument) && + "A value must be from instruction, constant or argument"); + + // …… + if (e) { + if (e_const) + ve = ConstantExpression::create(e_const); + else if (e_instr) + ve = valueExpr(e_instr, pout); + else if (e_argument) { + ve = search_ve(e_argument, pout); + assert(ve && "argument-expression should be there"); + } else { + abort(); + } + } else + ve = valueExpr(instr, pout); +} +``` ### 值编号的维护 @@ -269,7 +371,7 @@ struct CongruenceClass { > > 关键在于:index不应该递增,对于`%op12`,第二次迭代明明是同一个位置的定值,应该享有相同的值编号。 -一个目标清晰起来:我要保证,不同迭代中,同一位置生成的等价类要享有相同的值编号,以标示同一个等价类。这里的同一个不是指完全相同,举例如下: +一个目标清晰起来:我要保证,不同迭代中,同一位置生成(这里指新生成)的等价类要享有相同的值编号,以标示同一个等价类。这里的同一个不是指完全相同,举例如下: ``` // partition: {} @@ -308,7 +410,7 @@ y = 1 实现上要注意两点: -- 每次调用针对指令的转移函数之前,都要重新设置值编号 +- 每次调用针对指令的转移函数之前,都要归位值编号 - `Intersect(Ci, Cj)`中可能发现两个等价类不是相同的值编号,这时要新建phi等价类,并赋予一个精准的值编号:两个前驱中的定值,分别对应一个值编号,选择遍历顺序靠前的那个。 @@ -316,15 +418,13 @@ y = 1 ### 常量传播的实现 -### 所见非所得 +在函数`constFold_core(count, partition, instr, oprands)`中,程序会检查逐个检查oprands的类型,如果全部是`Constant*`类型,或者所在的等价类是`ConstantExpression`类型,那么就可以进行常量折叠,函数会返回一个`Constant*`变量。 -- 论文中伪代码对于基本块的关注不是很多,但是我们的实现一定要围绕基本块进行,这个的解决对策说到了,其实和单条语句没有本质差异。 +上述函数是在`valueExpr`中调用,此外在`valuePhiFunc`中也有进行常量折叠的代码:例如,一种情况下,对于`phi(a,b) op phi (c,d)`,程序会分别在左分支寻找`a op c`,常量折叠的部分在于,程序会试图合并`a op c`再去做分支寻找,如果合并不成功再使用`OP_expresion`寻找。 -- 我们针对lightir优化,经常深入API中陷入到实现细节,还容易被C++的语言特性绊住,但是应该尝试从直观上理解:我们究竟要实现什么样的效果。这个就得去看ll文件找灵感。然后这还不够,回过头来看代码又呆住了,因为ll语句和那些类型对应不上,这个就是容易忽视的一点:看lightir类型的print函数,这个能帮我们直观地串联起实现细节和表象的ll文件。 +### 纯函数分析的实现 -- 到了实验的后半阶段,调试信息的解读就变得越来越重要了。感谢某位同学劝我不要在用gdb debug,而是改用vscode,用gdb确实太慢了。 - - 这里调试的困难其实不在于调试的技巧,更多是信息解读,很多时候,用了半个小时甚至一上午才明白错误的原因在哪里,这也来源于对算法的理解不足。 +`CallExpression`的设计使得只要`equiv`返回true,就是同一条指令或者等价的纯函数调用,因此可以看作正常的冗余去除。 ## 实验设计 @@ -332,16 +432,155 @@ y = 1 实现思路,相应代码,优化前后的IR对比(举一个例子)并辅以简单说明 -### 思考题 +举一个例子,涉及到基本二元运算的荣誉、常量传播和纯函数分析: + +```cpp +int f(int i) { return i + 1;} +int main(void) +{ + int a; int b; int c; int d; + + /* 纯函数冗余和常量传播 */ + a = f(1+0); + b = f(2-1); + + c = a; + d = b; + + /* 二元元算冗余 */ + output(a+b); + output(c+d); + + return 0; +} +``` + +如下经过`mem2reg`,`main`函数的IR如下,简单地举一下会有的优化: + +- 常量传播:`%op0`、`%op2` + +- 纯函数分析:`%op1`、`%op3` + +- 二元运算冗余:`%op4`、`%op5` + +```cpp +define i32 @main() { +label_entry: + %op0 = add i32 1, 0 + %op1 = call i32 @f(i32 %op0) + %op2 = sub i32 2, 1 + %op3 = call i32 @f(i32 %op2) + %op4 = add i32 %op1, %op3 + call void @output(i32 %op4) + %op5 = add i32 %op1, %op3 + call void @output(i32 %op5) + ret i32 0 +} +``` + +经过gvn后, + +```cpp +define i32 @main() { +label_entry: + %op1 = call i32 @f(i32 1) + %op4 = add i32 %op1, %op1 + call void @output(i32 %op4) + call void @output(i32 %op4) + ret i32 0 +} +``` + +## 思考题 1. 请简要分析你的算法复杂度 + + 假设: + + - n为等价类个数 + + - v为指令总数 + + - j为汇合点个数 + + 先分析`join`的复杂度:因为对等价类逐对比较,所以有O(n^2^)次求交,每次求交假设是线性的,即O(v),则有`join`的复杂度为O(n^2^v) + + 分析`valueExpr`的复杂度:主要是搜索子表达式O(n) + + 分析`valuePhiFunc`的复杂度:`getVN` O(n)+递归调用O(nj) + + 再分析每条指令的`transferFunction`复杂度:克隆分区O(n)+寻找并擦除x O(n)+`valueExpr`O(n)+`valuePhiFunc`O(nj)+遍历寻找相同表达式O(n) + + 回到最顶层的函数`detectEquivalences`,在依次迭代中: + + - 初始化值编号:遍历全部指令,O(v) + + - 对所有块分析partition,包括: + + - j个汇合点的`join`,O(n^2^vj) + + - v条指令的`transferFunction`,O(nvj) + + 而至多迭代v次,即一个超大等价类拆分为每条指令一个等价类,所以总的时间复杂度是:O(n^2^v^2^j) + 2. `std::shared_ptr`如果存在环形引用,则无法正确释放内存,你的 Expression 类是否存在 circular reference? -3. 尽管本次实验已经写了很多代码,但是在算法上和工程上仍然可以对 GVN 进行改进,请简述你的 GVN 实现可以改进的地方 + + 无。 + + 首先从最终效果来看,打印的过程会递归调用print,环形引用会导致print死循环,从结果看是不存在环形引用的。 + + 其次分析程序运行时,expression的形成: + + - 一般的情况下,SSA格式会使得操作数都来自于前边的指令(大部分是单向的连线),包括copy statement + + - 可能出现问题的只是求交时, + +3. 尽管本次实验已经写了很多代码,但是在算法上和工程上仍然可以对 GVN 进行改进,请简述你的GVN实现可以改进的地方 + + 确实呀…… + + - 代码可以优化逻辑,使用模板、类继承等,提高实现的优美感 + + - 我理解最差的函数主要是Intersect,所以这里存在bug的可能性最大 + + - 关于全局变量、形参的相关的逻辑比较暴力,复杂度比较高。 + + 比如实验要求分区中存在全局变量和形参,我的方法是把对应的分区赋值给pin[Entry],但是也完全可以在结束时在追加,这样省去了每次求交时的对这两项的冗余遍历。 ## 实验总结 -此次实验有什么收获 +实验很难,体现在: + +- 算法本身就不理解(做完了也不是完全理解) + +- lightir的接口设计为了实现的简介,使得有些地方乍看起来就不是直观的,如op+cmpop才构成比较指令类型的充分信息 + +- 还有找bug难、理解bug难、去除bug也难 + +但还是做完了,做实验的过程中,有些地方看似不可逾越,但还是搞定了,我觉得值的总结一下: + +- 我们针对lightir优化,经常深入API和实现细节中,还容易被C++的语言特性绊住,但是应该尝试从直观上理解:我们究竟要实现什么样的效果。这个就得**去看ll文件找灵感**。然后这还不够,回过头来看代码又呆住了,因为ll语句和那些类型对应不上,这个就是容易忽视的一点:**看lightir类型的print函数**,这个能帮我们直观地串联起实现细节和表象的ll文件。 + +- 到了实验的后半阶段,调试信息的解读就变得越来越重要了。感谢某位同学劝我不要在用gdb debug,而是改用vscode,用gdb确实太慢了。 + + 这里调试的困难其实不在于调试的技巧,更多是信息解读,很多时候,用了半个小时甚至一上午才明白错误的原因在哪里,这也来源于对算法的理解不足。 + +关于报告,我每次编译的实验都会边写边记录(除了lab1和lab4-1), + +一方面,每次开始实验真的都是一头雾水,会有很多思绪乱糟糟的,记下来会帮我理清很多, + +另一方面,很多被折磨的地方,在结束实验时反而记不起来了,怎么搞定各种困难,怎么针对性提出方案,这才是实验中有价值的点。 + +所以我每次交报告都是又臭又长,不好意思嘿嘿:laughing::shushing_face:我加了目录的,希望助教不烦我~ ## 实验反馈(可选 不会评分) -对本次实验的建议 +搞个这么个大实验,我觉得很好啊,我很享受整个过程。 + +但是课程组应该压力很大的,尤其是当有同学提到:“实验时长已经超出课程设计”、“这学期其他所有课程实验加起来,都没编译耗时多"…… + +他们的批评有道理,但是我不认可拿这个给老师、助教施加压力,我看到的是实力强、负责任的老师和助教,实验任务量大,但不应该成为喷斥的集中点。这样认真设计的实验可不多呀!! + +站在我的角度,看print函数帮助很大,能帮我们把效果和实现对应起来, + +因此建议在lab4-1或者lab3中加入相关阅读任务,包括但不限于print,比如一条ll指令是怎么打印出来的,给定一个`Value*`变量,如何判断其是不是一个常量类型等。 diff --git a/src/optimization/GVN.cpp b/src/optimization/GVN.cpp index 378a382c7d2696a994374b10e229528a6d4eb272..c183bc7ce8dc424b9791a850b049b8a75d544679 100644 --- a/src/optimization/GVN.cpp +++ b/src/optimization/GVN.cpp @@ -634,7 +634,7 @@ GVN::transferFunction(Instruction *instr, Value *e, partitions pin) { // auto e_global = dynamic_cast(e); auto e_argument = dynamic_cast(e); assert((not e or e_instr or e_const or e_argument) && - "A value must be from an instruction or constant"); + "A value must be from instruction, constant or argument"); // erase the old record for x std::set::iterator it; for (auto c : pout)