提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
前言
B站硬核课程:
https://www.bilibili.com/video/BV1Bp4y1t7Cw?vd_source=e0b3cb923dbb83260e674c055a5ec68f
对应课本:编译器设计
一、overview
结构和组件
front end
symbolic information(变量名) ----front end----> abstract syntax tree
debug时候需要保留symbolic information
- scanner。
把一个string解析成一个一个的token。
判断一个token结束的方法:用正则的方式定义variable name的规则, - parser
把token构成一句statement 。检验grammar - semantic analysis
保证statement meaningful。
在symbol table里找对应的type,来判断是否meaningful。
Optimizer
理想上应该是backend通用的。
- Common subexpression elimination(CSE)
x = a + b + c;
y = d / (a + b);
----->
t = a + b;
x = t + c;
y = d / t;
这种不一定能优化,t如果是在memory(t定义后面有很多code才到使用的地方),那就增加了读写操作;t如果在register里,t需要在寄存器里待很久,其他变量可用寄存器就变少了。
但是实际上,并不是独立于backend的。
Backend
取决于machine。
ILOC
Instruction selection
从AST到basic assembly,决定machine Instruction顺序或者策略。
比如,可以load a 到r1,或者直接从memory中读a。
AST,depth first。
此处的register是虚拟的,并认为是无穷的。
Instruction Scheduling
例子:x = a * b + c / d;
假设:
load 时延3cycle
mul 时延2 cycle
div 时延 5 cycle
store 时延2 cycle
add 时延1cycle
start cycle | end cycle | live register | |||
---|---|---|---|---|---|
1 | load a -> r1 | 1 | 3 | r1 | |
2 | load b -> r2 | 2 | 4 | r1,r2 | |
3 | mul r1, r2 -> r3 | 5 | 6 | r3 | 取决于1 和2 |
4 | load c -> r4 | 6 | 8 | r3,r4 | |
5 | load d -> r5 | 7 | 9 | r3,r4,r5 | |
6 | div r4, r5 -> r6 | 10 | 14 | r3,r6 | 取决于4和5 |
7 | add r3, r6 -> r7 | 15 | 15 | r7 | 取决于6和3 |
8 | store r7 -> x | 16 | 17 | none |
例子总时延:17cycle。需要3个寄存器。
instruction level parallelism (ILP),把独立的long latency的放在前面:
start cycle | end cycle | live register | |||
---|---|---|---|---|---|
1 | load a -> r1 | 1 | 3 | r1 | |
2 | load b -> r2 | 2 | 4 | r1,r2 | |
3 | load c -> r4 | 3 | 5 | r1,r2,r4 | |
4 | load d -> r5 | 4 | 6 | r1,r2,r4,r5 | |
5 | mul r1, r2 -> r3 | 5 | 6 | r3,r4,r5 | 取决于1 和2 |
6 | div r4, r5 -> r6 | 7 | 11 | r3,r6 | 取决于3和4 |
7 | add r3, r6 -> r7 | 12 | 12 | r7 | 取决于5和6 |
8 | store r7 -> x | 13 | 14 |
14 cycle。需要4个寄存器。
NP-complete problem,具体怎么优化需要取决于backend。
- out-of-order processor,processor会自动做parallel,此时compile的目标是减少寄存器,减少spill。
- processor的限制,只能看到reorder buffer里面的指令。compiler可以看到更多scheduling reign的指令
Register Allocation
把basic assembling里面涉及的虚拟register,映射到物理寄存器。
register不够用的时候,额外做临时不用值的store和load到堆,叫做spill。minimize spilling为主要任务。
二、Scanner
识别regular language的工具。
deterministic finite automater(DFA)和NFA
DFA:在字母表里,每一个symbol有且只有一个transition。
- 例子:目标找101的字符串的DFA
包含101子串的NFA:
- 例子2:包含101或111子串
scanning会先构一个DFA,transition table,固定的swich case就可以了。但是NFA可以更好的表达RE。
Thompston Construction,把正则转NFA
正则RE
例子:
-
unsigned integer的RE:[1-9][0-9]* U 0
-
floating point number with an optional decimal point
([1-9][0-9]* U 0 )((.[0-9]*) U ε) -
变量名命名
(_U[a-Z])([a-Z] U [0-9]U_)*
RE 转 NFA
Thompson contruction
regular operation(NFA的拼接操作,改变accept state):union,concatenation,star(重复0或多次)。
star的NFA:
例子,a(b U c)*
的NFA:
NFA 转 DFA
例子,包含11或者101的子串:
按照NFA写的trace:
有D0, D1, D2, D3, D4 set of states,下一步需要把set of states改写成DFA。
例子1,将下面这个NFA改成DFA:
第一步:
此时,n1的是unreachable的,简化后的DFA:
例子2:
接上面a(b U c)*
的NFA转DFA:
把n?组成有可能性的state组合:
用state改写的DFA:
三、Parsing
scanning是定位word,parsing是定位statement的。
Parsing Tree
context-free grammar
grammar包括(V, T, P, S),分别是variables,terminals,productions,start variable。
context-free grammar,指的是,左边的是一个单独的variable
例子1:
下面例子将一个grammar改写成derivation tree:
根据这个语法写的例子aabbbb,有两种解析方法:
S->S1S2
S1->aS1b|ε
S2->bS2|b (因为m>n)
方法二:
S->aSb|B
B->bB|b(因为m>n,所以不能为空)
例子2:
classic expression grammar
Assign -> ident = Expr
Expr -> Expr + Term | Expr - Term | Term
Term -> Term * Factor | Term / Factor | Factor
Factor -> ident | num | (Expr)
x = (a + b) * c - d
的derivation tree:
左循环的grammar:X -> Xa | b
,等价于右循环 X -> b X', X' -> aX' | ε
。
// TODO 很多parse的算法
把parsing tree转成AST
syntax-directed translation
例子
x = a * 3 + b
构建AST
改写的AST,从叶子节点开始,只保留真正的Operation:
grammar和对应的action:
YACC notation,parser generator,只需要写grammar和action,会自动生成AST。左边的用两个$$,右边用一个$。$符号指的是一个node。叶子节点,不link其他node,用token表示。
其中,$$ = MakeMulNode($1, $2)
,拆开来看含义是:
MakeMulNode(Node *n1, Node *n2)
create new node with ptr n
n->left = n1
n->right = n2
return n
reduce
估计运算cost
每个operation的运算时间:
再根据parsing tree,从叶子节点开始往根节点推导。