Skip to content

Commit

Permalink
update contest docs
Browse files Browse the repository at this point in the history
  • Loading branch information
Ksitta committed Sep 6, 2024
1 parent 634f901 commit a035f68
Show file tree
Hide file tree
Showing 7 changed files with 114 additions and 72 deletions.
27 changes: 15 additions & 12 deletions docs/contest/intro.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,23 @@

大实验编译器目标:完成一个具有编译优化功能的高性能编译器。

为了简化课程实验,我们的基础实验框架在设计时并未考虑大实验的需求(例如:IR的类型系统简易
为了简化课程实验,我们的基础实验框架在设计时并未考虑大实验的需求(例如:IR的类型系统简易、没有区分基本块),在现有框架的基础上重构实现编译优化并不现实。因此,参加大实验的同学应该需要自己从头设计一个符合minidecaf 规范的编译器,包括前端、中端和后端。

## 参考实现进度及顺序
大实验为组队实验,4人一组(可以更少,但是评分标准保持不变),没有特殊情况时,同组同分。

1. 设计IR (一周)
这一步也是需要进行代码编写的,可以参考基础实验框架的IR在代码层面是如何实现的(`utils/tac`)。
## 参考实现进度及顺序

2. 完成前端 & 后端(一个月)
1. 编写前端、设计IR、完成中间代码生成 (两周)
- 前端:你可以使用现有的框架完成前端(如:Antlr、Flex & Bison)辅助你生成AST,完成词法分析、语法分析、语义分析以及中间代码生成。如果你想在这个过程中锻炼你对分析方法的理解,你可以自己实现LR(1)、LL(1)等分析器。
-设计IR也是需要进行代码编写的,可以参考基础实验框架的IR在代码层面是如何实现的(`utils/tac`)。
- 中间代码生成:将AST转换为IR,你可以参考基础实验框架的中间代码生成部分(`frontend/tacgen`)。

- 后端:先实现一个简单的寄存器分配方案,保证编译器能够正确运行,然后再考虑优化。

前端和后端是可以并行进行的,只要你们遵循相同的IR设计,前后端的开发是可以分开的。
2. 完成后端(两周)
- 先实现一个简单的寄存器分配方案,保证编译器能够完成全流程的运行,然后再考虑优化。

4. 增加中端优化和后端优化(剩下的时间)
- 中端优化:死代码消除、常量传播、复写传播、循环不变量外提等等
- 后端优化:图染色寄存器分配、现行扫描法、指令折叠等等
- 后端优化:图染色寄存器分配、线性扫描法、指令折叠等等

## 评分方法

Expand All @@ -29,8 +29,11 @@
- 选项一 完成竞赛第二阶段的优化编译器,替代期末考试

成绩占比 90%,剩余 10% 为书面作业和日常成绩。

你需要通过 stage 1-6 的所有测试样例以及附加测试的测试样例,这样你可以获得 50% 的正确性得分。剩下的 40% 的得分将根据你的编译器的性能进行评分。

其中这90%构成为:
- 50% 正确性测试:你需要通过 stage 1-6 的所有测试样例以及附加测试的测试样例,这样你可以获得 50% 的正确性得分。
- 10% 报告,介绍你的编译器的设计、你们进行的优化以及每个人完成的功能。
- 30% 性能测试,将根据你的编译器的性能进行评分。

性能评分方案:
附加测试中`performance`部分测试样例,以gcc打开`-O2`优化的性能的 60% 为满分,按照比例折算。如果一个程序gcc编译后运行时间为 12s ,如果你的程序执行时间为 20s 即为满分。
Expand All @@ -39,7 +42,7 @@
```
min{100, 100 * GCC编译程序运行时间 * 1.25/你的程序运行时间}
```
所有测试点取**几何平均值**,最后结果 * 40% 作为你的最终性能测试成绩。
所有测试点取**几何平均值**,最后结果 * 30% 作为你的最终性能测试成绩。
你也可以选择参加期末考,那么你的成绩将会是评分方案一、二取最高的一个。
Expand Down
2 changes: 2 additions & 0 deletions docs/contest/midend/cp.md
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
# 常量传播

常量传播的目的在于发掘代码中可能存在的常量,尽量用对常量的引用替代对虚拟寄存器的引用(虚拟寄存器和变量是同一个概念,以下都使用变量),并尽量计算出可以计算的常量表达式。
43 changes: 35 additions & 8 deletions docs/contest/midend/dce.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,42 @@
# 死代码消除

死代码消除即无用代码消除,死代码和不可达代码是两个概念。前者指的是执行之后没有任何作用的代码(例如:多余的计算),后者指的是永远无法被执行到的代码。
死代码消除(Dead code elimination, DCE)即无用代码消除,死代码和不可达代码是两个概念。前者指的是执行之后没有任何作用的代码(例如:多余的计算),后者指的是永远无法被执行到的代码。

活跃变量分析为每个程序点计算出所有变量的集合的子集,用于表示该点处活跃的变量,所以数据流分析的值集为所有变量的集合的幂集。"活跃"的含义是在程序执行过这一点**之后**,这个变量**当前的值**会被使用到,所以数据流分析是后向的。对于单个语句$S$,传递函数要根据$S$之后活跃的变量计算$S$之前活跃的变量,计算方法为:所有$S$用到的变量在$S$之前都是活跃的,所有$S$之后活跃的变量,如果没有在$S$中被定值,证明未来的那次使用用的还是$S$之前的值,所以也是活跃的
死代码消除通常依赖于Use-Def和Def-Use数据流分析([这里](https://people.cs.vt.edu/ryder/415/lectures/machIndepOpts.pdf)有一个参考资料),这个数据流分析可以帮我们找到每个指令用到的变量是在哪里定义的

这里讲一下几个常见的需要注意的点
这里介绍一种 DCE 的方法(来源于《高级编译器设计与实现》(鲸书))

进行死代码删除的时候,如果一条语句**没有副作用**,而且它的赋值目标(如果有的话)不在$out_S$中,那么这条语句就可以删去
- 所谓的副作用,其实就是除了"改变赋值目标变量"之外,其他所有的作用。显然,tac中没有既没有副作用,同时也没有赋值目标的语句
- 你在实现的时候可以认为除了`a = call b`之外的所有有赋值目标的语句都是没有副作用的,对于`a = call b`,如果$a \notin out_S$,要求将它优化为`call b`
- 首先,标识所有计算**必要值**的指令。比如在函数中要返回(`return`)或输出(`print`)的值,或者它可能会对从函数外访问的存储单元有影响(全局内存访问,对函数外定义的数组访问)。
- 然后,以迭代的方式逐步标记对这种对计算**必要值**有贡献的指令。假如一个指令的结果是另一个**必要值**计算指令的输入,那么这个指令也是必要的。
- 当以上迭代函数稳定不变时,所有未标记的指令都可以认为是Dead Code,可以删除。

其实还有一些语句的"副作用"不是很明确,比如除0,有符号整数溢出等(依平台而定),可能会导致程序崩溃,但是**优化的时候可以不把这当成是副作用**:按照c/c++常用的说法这叫未定义行为,这可以减轻编译器作者的负担,编译器可以假定程序永远没有未定义行为,并以此为依据来优化。
具体实现上,可以借助du/ud链来实现:

我们的测试样例中不会出现未定义行为,所以可以放心地忽略掉这些副作用。
- 维护一个set,存储所有必要值的定义指令。
- 找出函数所有的**必要值**,标记这些值的定义指令。
- 对于set中的每个指令,顺着ud链找到所有使用这个指令的指令,将这些指令加入set。
- 对于上一步中新加入的指令,继续顺着ud链找到所有使用这个指令的指令,将这些指令加入set。
- 重复上一步,直到set不再变化。
- 函数中的指令,如果不在set中,就可以认为是Dead Code。

此处举个例子:
```asm
_main:
_T0 = 1
_T1 = 2
_T2 = _T1 + 5
_T3 = _T0 + 2
_T4 = _T3 * 5
return _T4 # _T4 是必要值
```

顺着ud链,可以找到 `_T4 = _T3 * 5`,因此 `_T3` 也是必要值。继续找到 `_T3 = _T0 + 2`,因此 `_T0` 也是必要值。最终 `_T0``_T3``_T4` 都是必要值,而 `_T1``_T2` 的定义指令都可以认为是Dead Code。

因此可以优化为:
```asm
_main:
_T0 = 1
_T3 = _T0 + 2
_T4 = _T3 * 5
return _T4
```
56 changes: 51 additions & 5 deletions docs/contest/midend/midend.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# 中端介绍

中端的设计包括两个部分:中间表示的设计和中端优化
中端的设计包括:中间表示的设计、中端代码生成和中端优化

## 中间表示

前端的解析和中端设计密不可分,通常,我们需要设计一个中间表示(Intermediate Representation, IR)来连接前端和后端。也只有我们定义好了中间表示,前端才知道怎么处理AST
前端的解析和中端设计密不可分,通常,我们需要设计一个中间表示(Intermediate Representation, IR)来连接前端和后端。也只有我们定义好了中间表示,才能将来自于前端的AST转换为中端代码

### 什么是中间表示?

Expand Down Expand Up @@ -63,6 +63,47 @@ TAC 指令与汇编指令比较类似,每条 TAC 指令由操作码和操作

进一步地,你可以实现符合[静态单赋值](./ssa.md)要求的IR,静态单赋值的IR在编译器中有着广泛的应用,比如 LLVM 的 IR 就是一种静态单赋值的 IR。在静态单赋值的IR中,每个变量只被赋值一次,这使得编译器可以更容易地进行优化。

## 中间代码生成

前端解析后,我们会得到一棵抽象语法树,接下来我们需要将这棵抽象语法树转换为中间代码。依据你设计的IR,你需要在保证语义的情况下,将AST用你的IR表示出来。

如以下是一个简单的例子:

```C
int main(){
if(1){
return 1;
} else {
return 0;
}
}
```

生成的AST可能如下:
```
program [
function [
type(int)
identifier(main)
block [
if [
int(1)
block [
return [
int(1)
]
]
block [
return [
int(0)
]
]
]
]
]
]
```

## 中端优化

中端的优化是编译器的一个重要组成部分,它可以在保持程序功能不变的前提下,提高程序的性能。中端优化的目标是提高程序的性能,减少程序的运行时间和资源消耗。中端优化的方法有很多,比如常量传播、死代码消除、循环不变量外提、循环展开、函数内联等。
Expand Down Expand Up @@ -90,21 +131,26 @@ ret _T5
```

进一步如果我们进行死代码消除,可以得到:
> 什么是死代码消除?
> 死代码消除是指删除程序中没有用到的代码,以减少程序的运行时间和资源消耗。
```asm
_T5 = 35
ret _T5
```

我们在文档中对三个优化进行简单介绍,详见[复写传播](./rp.md)[常量传播](./cp.md)[死代码消除](./dce.md)
中端优化依赖与数据流、控制流分析,你需要先了解一些数据流分析的基础知识才能进行一些中端优化。

我们的文档里在[数据流分析](../../step6/dataflow.md)中对数据流分析进行了简单介绍,你可以在这里了解一些数据流分析的基础知识。除了这个文档中介绍的数据流分析,还有很多其他的数据流分析方法,比如Use-Def链、Def-Use链、可达定义分析等。

我们在文档中对两个优化进行简单介绍,详见[常量传播](./cp.md)[死代码消除](./dce.md)

## 中端参考资料

本章中我们以几个简单的例子介绍了什么是中间表示、中端优化以及如何做中端优化。此外我们也将会在这里给出一些中端优化的参考资料,供大家学习。

- [GCM & GVM](https://courses.cs.washington.edu/courses/cse501/06wi/reading/click-pldi95.pdf)
- [GCM & GVM](https://courses.cs.washington.edu/courses/cse501/06wi/reading/click-pldi95.pdf)

- [Engineering A Compiler](https://github.com/lighthousand/books/blob/master/Engineering%20A%20Compiler%202nd%20Edition%20by%20Cooper%20and%20Torczon.pdf)

- [LLVM IR](https://llvm.org/docs/LangRef.html)

Expand Down
36 changes: 0 additions & 36 deletions docs/contest/midend/rp.md

This file was deleted.

12 changes: 6 additions & 6 deletions docs/contest/midend/ssa.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,18 +33,18 @@ ret _T4
例如,考虑下面这段伪代码:

```assembly
y := 1
y := 2
x := y
y = 1
y = 2
x = y
```
很显然,其中变量 y 的第一次赋值是不必须的,因为变量 y 被使用前,经历了第二次赋值。对于编译器而言,确定这一关系并不容易,需要经过定义分析(Reaching Definition Analysis)的过程。在很多控制流复杂的情况下,上述过程将变得更加困难。

但如果将上述代码变为 SSA 形式:

```assembly
y1 := 1
y2 := 2
x1 := y2
y1 = 1
y2 = 2
x1 = y2
```
上述关系变得更加显而易见,由于每一个变量只被赋值一次,编译器可以轻松地得到 x1 的值来自于 y2 这一信息。

Expand Down
10 changes: 5 additions & 5 deletions docs/step14/example.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
### 实验指导 step14:静态单赋值
# 实验指导 step14:静态单赋值

本节实验指导使用的例子为:

Expand All @@ -15,15 +15,15 @@ int main() {
}
```

#### 词法语法分析
## 词法语法分析

mem2reg 属于在中间代码基础上的优化,因此词法语法分析部分没有额外增加的内容。

#### 语义分析
## 语义分析

mem2reg 属于在中间代码基础上的优化,因此语义分析部分没有额外增加的内容。

#### 中间代码生成
## 中间代码生成

mem2reg 使得我们可以在生成中间代码时,使用 Alloc、Load 和 Store 的组合针对局部变量生成符合 SSA 要求的代码。

Expand Down Expand Up @@ -78,6 +78,6 @@ _L2:
>
> Static Single Assignment Book 的 Chapter3:https://pfalcon.github.io/ssabook/latest/
#### 目标代码生成
## 目标代码生成

将 Phi 指令翻译为目标代码的过程相对复杂,本节实验不对这部分做要求。

0 comments on commit a035f68

Please sign in to comment.