Skip to content
GitLab
Projects
Groups
Snippets
Help
Loading...
Help
Help
Support
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
2
2022fall-Compiler_CMinus
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Service Desk
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Operations
Operations
Metrics
Analytics
Analytics
CI / CD
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
李晓奇
2022fall-Compiler_CMinus
Commits
60b6c163
Commit
60b6c163
authored
Dec 12, 2022
by
李晓奇
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
finish report!
parent
0afbec5c
Changes
3
Show whitespace changes
Inline
Side-by-side
Showing
3 changed files
with
338 additions
and
98 deletions
+338
-98
Reports/.gitignore
Reports/.gitignore
+1
-0
Reports/4.2-gvn/report.md
Reports/4.2-gvn/report.md
+336
-97
src/optimization/GVN.cpp
src/optimization/GVN.cpp
+1
-1
No files found.
Reports/.gitignore
View file @
60b6c163
...
@@ -2,3 +2,4 @@
...
@@ -2,3 +2,4 @@
2-ir-gen-warmup/*.pdf
2-ir-gen-warmup/*.pdf
3-ir-gen/*.pdf
3-ir-gen/*.pdf
4.1-ssa/*.pdf
4.1-ssa/*.pdf
4.2-gvn/*.pdf
Reports/4.2-gvn/report.md
View file @
60b6c163
# Lab4.2 实验报告
# Lab4.2 实验报告
姓名 学号
> 李晓奇 PB20111654
[TOC]
## 实验要求
## 实验要求
请按照自己的理解,写明本次实验需要做什么
依照伪代码,填充框架实现GVN.
## 实验难点
## 实验难点
...
@@ -34,37 +36,31 @@ detectEquivalences(G)
...
@@ -34,37 +36,31 @@ detectEquivalences(G)
-
怎么设计top?
-
怎么设计top?
尽管算法伪代码中,top是对每一条语句的赋值的,但是任意一条语句
的都有所属基本块,只要对基本块的pout标注top、就能保证基本块的pin合乎逻辑,逐条执行转移函数后,每条语句的pin都合法。
``
尽管算法伪代码中,top是对每一条语句的赋值的,但是任意一条语句
都有所属基本块,只要对基本块的pout标注top、就能保证基本块的pin合乎逻辑,逐条执行转移函数后,每条语句的pin都合法。
-
`while changes to any POUT occur`
-
`while changes to any POUT occur`
这句话如此轻松,但是在实现中出现了问题:这涉及到怎么判断分区相等,做不好会出现死循环。
这句话如此轻松,但是在实现中出现了问题:这涉及到怎么判断分区相等,做不好会出现死循环。
如果单纯比较index恐怕不行:因为我目前的`transferFunction`会生成一个新的等价类,赋予一个新的编号。
分区中的同一个等价类,集合中元素也可能发生了增减,在这里的语义下是不相等的。所以我使用对等价类中的
`members`
做相等的判断,如果两个分区中的等价类都能找到相等的,那么就判断分区相等。
所以我使用对等价类中的`members`做相等的判断,如果两个分区中的等价类都能找到相等的,那么就判断分区相等。
> 但是这样依赖于set的排序,
>
> - 对于分区,这个排序依赖于等价类中index的值,侥幸来说每条指令按顺序执行,即使index不一致(每次转移函数分配最新的index),大小顺序也是一致的?
>
> - 对于等价类,里边是`Value*`指针,这个指针的等价就是语义上的等价,没问题。
改过来后发现还是死循环,DEBUG很久意识到我的分区相等写的有问题:
`return members_ == other.members_`
,这样会挨个儿比较
`members_`
指针的值,而不是我预想的比较指针指向的等价类。所以要手动遍历一下。
改过来后发现还是死循环,DEBUG很久意识到我的分区相等写的有问题:
`return members_ == other.members_`
,这样会挨个儿比较
`members_`
指针的值,而不是我预想的比较指针指向的等价类。所以要手动遍历一下。
这个遍历也不是很直观的,因为要做两层解引用:
这个遍历也不是很直观的,因为要做两层解引用:
```cpp
```
cpp
for
(;
it1
!=
p1
.
end
();
++
it1
,
++
it2
)
for
(;
it1
!=
p1
.
end
();
++
it1
,
++
it2
)
if
(
not
(
**
it1
==
**
it2
))
if
(
not
(
**
it1
==
**
it2
))
return
false
;
return
false
;
```
```
`*it`
是等价类的指针,
`**it`
才是等价类。
`*it`
是等价类的指针,
`**it`
才是等价类。
除此之外,我还忘了`++it2`,淦。
> 这里不用逐对比较依赖于set的排序:
>
改完这些,终于能跑通第一版本了……
> 对于分区(等价类指针的set),小于号根据index重载,所以排序依赖于等价类中index的值。
>
> 如果分区结果已经收敛,那么由于遍历顺序一致,会有每条分区中的等价类具有相同的index排布,比较时正确true。
### 对于函数`Intersect(Ci, Cj )`
### 对于函数`Intersect(Ci, Cj )`
...
@@ -102,13 +98,22 @@ struct CongruenceClass {
...
@@ -102,13 +98,22 @@ struct CongruenceClass {
-
额外设计:赋予
**精准的**
值编号
-
额外设计:赋予
**精准的**
值编号
在我的
设计中,每条指令都有一个潜在的值编号,如果要为这条指令新建等价类,就使用这个值编号。所以需要`Intersect`确定新的值编号时,应该选用靠前的以为后边指令腾出编号空间。
在我的
[
设计
](
#值编号的维护
)
中,每条指令都有一个潜在的值编号,如果要为这条指令新建等价类,就使用这个值编号。所以需要
`Intersect`
确定新的值编号时,应该选用靠前的以为后边指令腾出编号空间。(详见
`Intersect`
中的
`exact_idx`
变量)
### 对于函数`valuePhiFunc(ve, P)`
### 对于函数`valuePhiFunc(ve, P)`
-
输入就一个分区,实际上要用到两个前驱的分区,怎么处理?
-
输入就一个分区,实际上要用到两个前驱的分区,怎么处理?
传入基本块做参数。
传入基本块做参数,函数声明改成:
```
cpp
std
::
shared_ptr
<
GVNExpression
::
PhiExpression
>
valuePhiFunc
(
std
::
shared_ptr
<
GVNExpression
::
Expression
>
ve
,
BasicBlock
*
bb
,
Instruction
*
instr
);
```
bb是当前块,用来找到两个前驱,instr用来做常量折叠,只有其op会被用到
-
二元运算的顺序颠倒怎么办?
-
二元运算的顺序颠倒怎么办?
...
@@ -116,11 +121,13 @@ struct CongruenceClass {
...
@@ -116,11 +121,13 @@ struct CongruenceClass {
>
>
> 对于第二种,单纯寻找x1+y2和y1+x2肯定不合逻辑
> 对于第二种,单纯寻找x1+y2和y1+x2肯定不合逻辑
论文中
有详细的版本:
吐槽这个论文离谱之时(省去诸多细节),突然发现论文其实
有详细的版本:
!
[](
figures/value_phi_func.png
)
!
[](
figures/value_phi_func.png
)
### 等价类设计
在这个基础上,我又加入了一些常量折叠的内容,见
[
后边的讨论
](
#常量传播的实现
)
### 等价类
> ```cpp
> ```cpp
> shared_ptr<Expression>
> shared_ptr<Expression>
...
@@ -129,71 +136,115 @@ struct CongruenceClass {
...
@@ -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
};
```
新增加的等价类及其含义如下:
| 等价类 | 含义 |
| --------------- | ------------------------ |
| cmp | 整形/浮点比较 |
| cast | 类型转换 |
| gep | GEP指令 |
| unique | 不做特殊处理但会用到的,如load、alloca |
| global、argument | 全局变量和形参 |
| call | 函数调用 |
#### 等价类设计
首先讨论这些等价类设计,在实验中发现,这些设计主要是要保存足够的信息,能够支持我们做正确的相等判断,而这些信息又和lighit的设计/接口紧密关联:
-
e_constant
> 常量折叠会在后边详细探讨
常量等价类,这个在常量折叠中起关键作用,其中保存一个
`Constant*`
变量。
-
e_bin
二元运算的构成:左右操作数+操作符。
-
e_cmp
lightir中,比较指令的操作符并不是单一的,他是一个两层的信息:op+cmpop,所以设计中
`CmpExpression`
继承自
`BinaryExpression`
后再加入记录cmpop的信息。
-
e_phi
phi表达式的构成只需要两个操作数即可。
-
e_cast
操作符是三层的信息:op+源类型+目的类型。
此外需要记录(源)操作数的expression。
指令有多种类型,也即lightIR中有多种赋值方式,而非单纯的伪代码中的二元运算。但是即使是已经在算法中讨论过的二元运算,写起来也不容易。
-
e_gep
#### 生成等价类的逻辑
GEP表达式的结构:基地址(expression)+索引(vector
\<
expression
\>
)
首先讨论生成等价类的逻辑:
-
e_unique
- 二元运算`y op z`
这个表达式是为了对应load和alloca,其左值都不需特别处理,但是又应该有表达式。
如果y、z都是常量,那么可以进行常量折叠,得到**一个**`ConstantExpression`类型
。
使用一个
`Instruction*`
变量保存左值,做比较用
。
否则返回的是一个`BinaryExpression`类型,这个类型需要左右操作数,都是`Expression`类型,如果操作数是常量,使用`ConstantExpression`创建新的值表达式,否则递归调用`valueExpr`
。
还加入了index,这个表达式是树的叶子节点,我希望它输出是v~index~这样
。
> 这里为什么递归调用不会走到未定义?
-
e_global和e_argument
- phi函数
全局变量、形参类型也要有归属的表达式,但是和上述的unique不兼容,所以各自新建等价类,直接保存对应的指针。这样分开也方便控制打印输出。
我们逻辑上把其改变成了copy,`transferFunction`应该维护这一点,所以认为没有phi函数传入。
-
e_call
具体是`transferFunction`函数中的这几句话:
函数调用算是最复杂的一个,涉及到纯函数分析,还有函数类型、实参比较等很多需要考虑的。在这个表达式中我记录了这些信息:
-
`pure_`
:是否是纯函数
-
`f_`
:
`Function*`
类型,用来判断是否是同一个函数
-
`args_`
:实参的
`expression`
容器。
再来看看比较的函数:
```
cpp
```
cpp
GVN::partitions
bool
equiv
(
const
CallExpression
*
other
)
const
{
GVN::transferFunction(Instruction *x, Value *e, partitions pin)
// single instruction, should be same
{
if
(
instr_
==
other
->
instr_
)
auto e_instr = dynamic_cast<Instruction *>(e);
return
true
;
auto e_const = dynamic_cast<Constant *>(e);
if
(
not
(
pure_
and
f_
==
other
->
f_
))
assert((not e or e_instr or e_const) &&
return
false
;
"A value must be from an instruction or constant");
for
(
int
i
=
0
;
i
<
args_
.
size
();
i
++
)
// ……
if
(
not
(
*
args_
[
i
]
==
*
other
->
args_
[
i
]))
if (e) {
return
false
;
if (e_const)
return
true
;
ve = ConstantExpression::create(e_const);
else
ve = valueExpr(e_instr, &pin);
} else
ve = valueExpr(x, &pin);
}
}
```
```
- 比较函数:和二元运算一致。
这里总有bug,最后的逻辑是:
- 类型转换:添加新的表达式类型`CastExpression`,处理和二元操作类似。
- 其他产生赋值的指令,也会由`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(); }
> ```
#### 若干需要梳理的问题
#### 若干需要梳理的问题
...
@@ -201,13 +252,13 @@ struct CongruenceClass {
...
@@ -201,13 +252,13 @@ struct CongruenceClass {
-
等价类中的ve一定存在吗?
-
等价类中的ve一定存在吗?
根据等价类的设计,ve一定是存在的,且等价类中`members_`中一定存在元素
。
根据等价类的设计,ve一定是存在的,且等价类中
`members_`
中一定存在元素
(如果删除某一个元素后,等价类为空,要连带着从分区删掉这个等价类)
-
ve一定唯一吗?
-
ve一定唯一吗?
从逻辑上,等价类集合中的元素享有共同的ve
产生的结果,
ve是唯一的
从逻辑上,等价类集合中的元素享有共同的ve
,所以
ve是唯一的
从实现上,添加
从实现上,添加
元素,添加元素的ve和等价类的ve是相等的,有种归纳法的感觉,总之ve应该是唯一的。
-
ve或者vpf能够作为代表吗?
-
ve或者vpf能够作为代表吗?
...
@@ -215,19 +266,21 @@ struct CongruenceClass {
...
@@ -215,19 +266,21 @@ struct CongruenceClass {
> if ve or vpf is in a class Ci in POUTs
> if ve or vpf is in a class Ci in POUTs
可以的,等价类中的元素都有唯一的表达式,其他等价类即使形式相同,操作数也不会相同,所以等价类和表达式其实是一一对应的关系。
可以的,等价类中的元素都有唯一的表达式,其他等价类即使形式相同,操作数也不会相同
/等价
,所以等价类和表达式其实是一一对应的关系。
(这也解释了UNIQUE中传入index的合理性)
(这也解释了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_`
做判断吗?
-
等价类为空的判定,可以用
`members_`
做判断吗?
...
@@ -247,9 +300,58 @@ struct CongruenceClass {
...
@@ -247,9 +300,58 @@ struct CongruenceClass {
BB3: ... ; preds = BB1, BB2
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中等价类分别取交,
执行
`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。
因为最终反映到IR上,我们直接关心的都是`members_`中的成员,所以用`members_`的空代表等价类的空我觉得是合适的。
`transferFunction(x, e, pin)`
中对应的处理(如何生成相关的等价类):
```
cpp
GVN
::
partitions
GVN
::
transferFunction
(
Instruction
*
x
,
Value
*
e
,
partitions
pin
)
{
auto
e_instr
=
dynamic_cast
<
Instruction
*>
(
e
);
auto
e_const
=
dynamic_cast
<
Constant
*>
(
e
);
// auto e_global = dynamic_cast<GlobalVariable *>(e);
auto
e_argument
=
dynamic_cast
<
Argument
*>
(
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 {
...
@@ -269,7 +371,7 @@ struct CongruenceClass {
>
>
> 关键在于:index不应该递增,对于`%op12`,第二次迭代明明是同一个位置的定值,应该享有相同的值编号。
> 关键在于:index不应该递增,对于`%op12`,第二次迭代明明是同一个位置的定值,应该享有相同的值编号。
一个目标清晰起来:我要保证,不同迭代中,同一位置生成的等价类要享有相同的值编号,以标示同一个等价类。这里的同一个不是指完全相同,举例如下:
一个目标清晰起来:我要保证,不同迭代中,同一位置生成
(这里指新生成)
的等价类要享有相同的值编号,以标示同一个等价类。这里的同一个不是指完全相同,举例如下:
```
```
// partition: {}
// partition: {}
...
@@ -308,7 +410,7 @@ y = 1
...
@@ -308,7 +410,7 @@ y = 1
实现上要注意两点:
实现上要注意两点:
- 每次调用针对指令的转移函数之前,都要
重新设置
值编号
-
每次调用针对指令的转移函数之前,都要
归位
值编号
-
`Intersect(Ci, Cj)`
中可能发现两个等价类不是相同的值编号,这时要新建phi等价类,并赋予一个精准的值编号:两个前驱中的定值,分别对应一个值编号,选择遍历顺序靠前的那个。
-
`Intersect(Ci, Cj)`
中可能发现两个等价类不是相同的值编号,这时要新建phi等价类,并赋予一个精准的值编号:两个前驱中的定值,分别对应一个值编号,选择遍历顺序靠前的那个。
...
@@ -316,15 +418,13 @@ y = 1
...
@@ -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
...
@@ -332,16 +432,155 @@ y = 1
实现思路,相应代码,优化前后的IR对比(举一个例子)并辅以简单说明
实现思路,相应代码,优化前后的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.
请简要分析你的算法复杂度
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?
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*`
变量,如何判断其是不是一个常量类型等。
src/optimization/GVN.cpp
View file @
60b6c163
...
@@ -634,7 +634,7 @@ GVN::transferFunction(Instruction *instr, Value *e, partitions pin) {
...
@@ -634,7 +634,7 @@ GVN::transferFunction(Instruction *instr, Value *e, partitions pin) {
// auto e_global = dynamic_cast<GlobalVariable *>(e);
// auto e_global = dynamic_cast<GlobalVariable *>(e);
auto
e_argument
=
dynamic_cast
<
Argument
*>
(
e
);
auto
e_argument
=
dynamic_cast
<
Argument
*>
(
e
);
assert
((
not
e
or
e_instr
or
e_const
or
e_argument
)
&&
assert
((
not
e
or
e_instr
or
e_const
or
e_argument
)
&&
"A value must be from
an instruction or consta
nt"
);
"A value must be from
instruction, constant or argume
nt"
);
// erase the old record for x
// erase the old record for x
std
::
set
<
Value
*>::
iterator
it
;
std
::
set
<
Value
*>::
iterator
it
;
for
(
auto
c
:
pout
)
for
(
auto
c
:
pout
)
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment