编译器介绍 --- 原理篇

编译器介绍 — 原理篇

这学期在学编译器,谨以此博客记录一下所学知识,并且也作为一个编译器的简单入门教程。

系列文章

概述

首先我们需要对编译器有一个宏观的把握,可以想象,编译器本身一定是一个很庞大的软件,所以编译器大概率是分为多个模块的(模块化!),每个模块只负责某一方面的特定功能。整个编译器可以用如下这个图(我们的教授形象的称之为,compiler mountain)来展示。

compiler mountain
整个编译器大体上可以被分为"前端"和"后端"两大部分:

  • “前端” — “source language specific”,也就是和源代码使用的语言紧密相关的
    • 负责分析 (analysis),检查词法,语法,语义等错误,并产生IR树
    • 包含4大模块:词法分析 (Lexical analysis),语法分析 (Syntax analysis),语义分析 (Semantic analysis),翻译 (Translate)
  • 汇合点 — IR(intermediate representation)树
    • 前后端交汇的地方,与源程序语言无关,也与目标ISA架构无关
  • “后端” — “ISA specific”,ISA代表Instruction Set Architecture,也就是和我们最后编译生成的汇编语言相关的(如:MIPS,x86等不同架构)
    • 负责综合 (synthesis),将IR树转化为符合目标架构的汇编程序
    • 包含3大模块:指令选择 (Instruction Selection),实时变量分析 (Liveness Analysis),寄存器分配 (Register Allocation)

注:本系列文章使用的教材为"Modern Compiler Implementation in ML",编译器开发语言为SML,目标编译语言为Tiger(该书提出的一个简单语言),虽然不是C语言,但是相关知识都是通用的,只不过相对简化了一些。同时由于阅读学习时候使用的是英文教材,所以很多专有名词会使用英文来表示。

相关资料:

前端

Lexer

  • 输入输出:source program -> "Lexer" -> Tokens
  • 作用:将源程序分解成一系列的tokens(如:程序里面的str转换为Tokens.STR)
  • 介绍:Lexer又叫词法分析器,负责将源程序Tokenize的同时进行词法分析 (Lexical analysis),词法错误(如关键词为var但是写成了val)将会在这个阶段被捕捉并报告
  • 相关知识点:正则表达式 (regular expression),有限状态机 (FSA, finite state automaton)

Tokens就是一个程序的最小组成单元,比如a = b + c这行代码,就会转化为ID(a) ASSIGN ID(b) PLUS ID(c)这5个tokens。那么为什么要将一个程序转化为一列的token而不是直接处理?简化!抽象!

  1. 简化:一个程序里面有些内容是编译器不关心的,比如注释,所以就可以在词法分析这一步上,将这些(对编译器而言)无用的东西全部去掉;
  2. 抽象:利用token作为一个中间变量,就可以将两个组件(lexer和parser)连接起来,lexer不需要考虑后端所有的处理,只需要负责将源程序转化为一系列的tokens,同样的parser不需要去考虑源程序的结构等信息,只需要处理lexer生成的tokens;从而使得每个组件只负责一个特定的功能,并且不同组件间有一个合适的“交流”方式

Parser

  • 输入输出:Tokens -> "Parser" -> AST
  • 作用:解析程序的语法结构(如:哪些token一起构成了一个表达式)
  • 介绍:Parser又叫语法分析器,负责将一系列的tokens组合成有意义的程序语句并同时进行语法分析 (Syntax Analysis),语法错误 (如:括号不匹配,漏了分号等)将会在这个阶段被捕捉并报告
  • 相关知识点:上下文无关文法 (CFG, Context Free Grammar)

首先我们来看一下什么是AST (Abstract Syntax Tree,抽象语法树),针对a = b + c这条语句,将其转换为AST的结果就是如下,一个等号节点,子节点分别为等号左右两边的内容。
AST
我们之所以要把源程序转换为树的结构,是为了方便后序进行各种分析(不需要再对字符串进行操作)。

然后我们再来看看实现,仔细研究一下你会发现,Parser实际上也是实现类似一个"匹配"操作,如找到ID(b) PLUS ID(c)这样的结构,然后转化为一个加法节点,到这里,你可能会好奇,既然也是一个“匹配”操作,那么我们可不可以继续使用正则表达式呢?答案是不可以,看以下这个例子:

这是一个正则表达式 (带有简化功能,即用digits代表[0-9]+

digits = [0-9]+
sum = (digits "+")* digits

sum可以用来匹配所有的加法表达式,如1 + 3 + 5。至此好像并没有什么问题,继续往下看:

digits = [0-9]+
sum = expr "+" expr
expr = "(" sum ")" | digits

还是加法表达式,只不过引入了括号,从而可匹配如(1 + 3)(2 + (4 + 5))。注意,我们虽然写的是正则表达式,但实际上工具内部是使用FSA(有限状态机)进行实现的,但是FSA不具备检测括号是否匹配的功能,因为一个N个状态的FSA是无法检测嵌套深度超过N的括号的。显然,单纯的“简化”并没有增加正则表达式的能力,也就是说并不能定义更多的"语言",除非支持“递归”,而这个正是我们parser需要的能力。

至此就可以引入我们的新工具 — CFG (Context Free Grammar),用来定义程序的语法。相比较于正则表达式,CFG具备递归的特性,一个CFG (上下文无关文法) 定义了一种"语言",一个文法包含一系列的productions,具有如下的格式:

symbol -> symbol symbol ... symbol

每一个production代表可以用箭头右边的东西替代箭头左边的东西,这里每一个symbol是以下几种情况之一:

  • terminal — 它是被定义语言中的一个token
  • non-terminal — 出现在了某些production的左手边

注意不能有任何token出现在production的左手边,同时其中一个nonterminal会被识别为起始symbol (通常是第一个production左手边的symbol),亦即从那个开始分析。

以下是一个例子,定义了数字间的乘法和加法:

E -> E + T
   | T
T -> T * F
   | F
F -> ( E )
   | num

为了得到num * num + num + num这样的一个表达式,我们可以从start symbol E开始,一步步替换:

E
E + T					(E -> E + T)
E + T + T				(E -> E + T)
T + T + T				(E -> T)
T * F + T + T			(T -> T * F)
F * F + F + F			(T -> F)
num * num + num + num	(F -> num)

所以Paser的任务就是根据程序的语法,构建出对应的CFG语句,这里哥大有一个pdf做了很好的总结,我们只需要根据这个翻译成对应的CFG即可。

Semantic Analysis

  • 输入输出:AST -> "Semantic Analysis" -> AST
  • 作用:分析程序的含义 (如:表达式是否合法,a+b里面ab是否均为int)
  • 介绍:Semantic Analyser又叫语义分析器,各种语义错误将会在这个阶段被捕捉并报告,该阶段主要有两个功能:
    • “逃逸"分析 (escape analysis) — 由于tiger语言允许嵌套定义函数,所以存在一种情况就是函数f2里面使用了一个在函数f1里面定义的变量a,这种情况我们称变量a"逃逸”,针对"逃逸"的变量,我们需要将其保存在frame里面而不是寄存器里面
    • 语义分析 (semantic analysis) — 分析程序是否存在语义错误,如:在循环外写break,将字符串赋值给一个int型变量等
  • 相关知识点:类型论 (Type Theory)

注意:Semantic Analysis是捕获程序静态错误(语法语义等错误,并不包含死循环,数组越界等动态错误)的最后一个阶段,也就是说只要一个程序通过了该阶段,那么我们可以保证该程序一定是一个语法上正确的程序(即可以编译成一个正确的汇编语言程序)。

Translate

  • 输入输出:AST -> "translate" -> IR
  • 作用:将与源程序语言相关的AST"翻译"成与源程序语言无关的IR
  • 介绍:Translate负责将AST转换为IR,保留了一定的原有特性(如仍是"树"结构),也将部分特性转换为和目标汇编一样(如不再有多种类型,所有变量均为32比特整型)
特性ASTIRAssembly
类型int, string, array…32 bit int32 bit int
结构tree of AST nodetree of IR nodesequence of instructions
变量范围嵌套范围 (不同函数嵌套)全局范围全局范围
可用变量~25
控制语句if, else, while…2 target jump1 target + fallthrough

汇合点

IR (intermediate representation)

  • 作用:连接编译器前后端
  • 介绍:IR是编译器前后端的汇合点,起到桥梁的作用,IR拥有自己的一套完整的抽象语法,该语法与前端的源程序和后端的ISA架构均无关

乍一看IR似乎显得比较多余,我们要先把AST转化为IR再把IR转化为具体的汇编指令,那么为什么不直接把AST转化为汇编指令呢?答案是,可以但会降低可拓展性。

针对我们这种情况 — 将tiger程序转化为MIPS,只有一种源语言和一种ISA,IR确实不是必要的,直接把AST转化为汇编指令甚至会更简单一点。那我们为什么还需要IR呢?可拓展性 & 解耦合,一个编译器可能面对将多种源语言,同时也可能需要将其转化为多种ISA(如:MIPS,x86等),假设我们有m种源语言,n种目标ISA,没有IR的话总共有 m ∗ n m * n mn个排列组合,但是引入IR之后,前端只需要考虑如何将源程序转化为IR,后端只需要考虑如何将IR转化为目标ISA,前后端解耦合使得只有 m + n m + n m+n个排列组合。
在这里插入图片描述

后端

Instruction Selection

  • 输入输出:IR -> "Canonicalize" -> "Instruction Selection" -> Infinite Registers MIPS
  • 作用:将IR树"规范化"后转换为对应的MIPS汇编程序
  • 介绍:
    • Canonicalize的代码书本已经给出,主要作用有3个
      • 线性化 (linearize) — 去掉所有的SEQ和ESEQ,并使得所有CALL指令的父指令均为EXP或者MOVE (即不存在一个CALL指令含有另一个CALL指令作为参数)
      • 模块化 (basic blocks) — 将程序代码模块化,每个模块最开始添加一个LABEL,并且模块一定以JUMP或CJUMP结尾
      • 重新排列 (trace schedule) — 将模块化后的程序进行重新排列,使得每一个CJUMP指令后面紧接着的一定是false LABEL (便于后面将其改为汇编语言"跳转 或 继续执行"的形式)
    • 指令选择 (instruction selection)
      • 方法:maximum munch — 将尽可能大的子树转化为一条指令 (见例子)
      • 注意有两个问题我们我们并不需要在这个阶段考虑 (会在register allocation阶段解决)
        • 寄存器数量问题 — 我们可以假设有无穷多个寄存器,所以每次遇到新的变量可以直接声明一个新的寄存器保存 (所以叫infinite registers MIPS)
        • move指令必要性问题 — 我们不需要考虑move指令有没有必要,会不会影响性能,比如每次调用一个函数,我们都默认会将返回值移动到一个新的寄存器里面move t168, $v0

下面我们借助一个例子来理解一下什么是"maximum munch",下面这是一颗IR树,我们的目标就是将其转化为尽可能少的汇编语言 (提高性能):

在这里插入图片描述
我们先将树的右半部分简化成一个单独的寄存器,得到下图上面的那个简化版的树。此时我们有两个选项 (都是可行的,性能有差异),1) 单独处理每一个节点,如左下,遇到一个CONST就先用一个li指令将其加载到一个寄存器内;2) 使用maximum munch,将尽可能大的子树转化为一条指令,如右下,整个子树可以转换为一个sw指令。

在这里插入图片描述

  • 左下结果
li  t100, 5         # 1
add t101, t100, 168 # 2
sw  t169, t168      # 3
  • 右下结果 — sw t169, 5(t168)

我们可以看到两种方式最后都实现了相同的功能,但是后一种方式明显性能会更好。使用相同的方法,将一开始的例子进行如下的划分:

在这里插入图片描述

并按顺序转换为汇编程序:

addi t170, $zero, 6   # 1
lw   t100, 8(t169)    # 2
mul  t101, t100, t170 # 3
sw   t101, 5(t168)    # 4

总结,从这个阶段开始,做的事情就是与ISA相关的了,需要我们对目标汇编语言的语法和语句有所了解,灵活运用使得能将一个完整的IR树转换为尽可能少的汇编程序。

Liveness Analysis

  • 输入输出:Infinite Registers MIPS -> "Liveness Analysis" -> Interference Graph
  • 作用:分析每个变量/寄存器的生命周期,从而产生一个相交图 (interference graph)
  • 介绍:Liveness分析主要是为了下一阶段的寄存器分配做准备,最后生成一个相交图,图中每个节点均代表一个寄存器 (亦即程序中的一个变量),如果一条指令写入了某个寄存器t100,则我们会在t100和该指令所有的live-out变量 (即后面的指令可能会使用到的变量)间都添加一条线,因为变量t100和所有的live-out变量不能同时保存在同一个寄存器内
  • 相关知识点:数据流分析 (Dataflow Analysis)

Liveness Analysis主要包含两个阶段,1) 生成CFG (control-flow graph),即控制流图; 2) 根据CFG和live-out信息,产生igraph (interference graph)

1) CFG + live-out

CFG就是控制流图,该图中每一个节点均为一个basic block (即程序只能从一个地方进入该block,另一个地方离开该block,换句话说每一个jump和branch指令都会终结一个basic block),节点间的连线代表了程序的可能执行路径。我们看一个简单的例子:

这是一个简单的tiger程序,声明了两个变量,然后一个if语句

let
	var x := 3
	var y := 4
in
	if x > y then x + 2 else y + 3
end

这是该程序对应的CFG (用汇编语言表示):
在这里插入图片描述
有了CFG之后,我们还需要计算每一个basic block的live-out变量,这里用到的一个方法就是"iterate to a fix point (迭代到固定点)",就是我们先假设每一个basic block的live-out一开始都是空集,然后根据最新信息,不断地更新这个结果,直到某一次迭代过程中没有发生任何改变。其中LiveIn和LiveOut的计算公式如下所示:使用一个变量,会产生liveness; 定义一个变量,会杀死liveness。由于最后一个block (含有jr $ra)没有后续的block,所以其live-out是空集。从哪一个节点开始迭代,最终都会得到同一个结果,但是假如从最后一个节点开始,顺着程序执行的反方向进行迭代,这样的收敛速度会最快 (liveness的信息是从后往前传递的)。这也就是我们所说的"数据流分析"。

  • L i v e O u t [ B ] = ⋃ S ∈ s u c c e s s o r [ B ] L i v e I n [ S ] LiveOut[B] = \bigcup_{S \in successor[B]} LiveIn[S] LiveOut[B]=Ssuccessor[B]LiveIn[S]
  • L i v e I n [ B ] = u s e [ B ] ∪ ( L i v e O u t [ B ] − d e f [ B ] ) LiveIn[B] = use[B] \cup (LiveOut[B] - def[B]) LiveIn[B]=use[B](LiveOut[B]def[B])

根据上述方法,我们可以计算出每一个block的live-out:
![在这里插入图片描述](https://img-blog.csdnimg.cn/20210425231235406.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2tld2VpMTY4,size_16,color_FFFFFF,t_70

2) interference graph

得到了CFG和live-out信息之后,我们就可以先构建一个空的igraph,包含所有需要的节点 (遍历所有指令即可得到),但是没有任何的连接,然后逐block的处理,添加连接。这里以第一个block为例:
在这里插入图片描述
从最后一条指令开始,用同样的公式,计算出每一条指令的live-out,然后在每一条指令定义/写入的变量和该条指令的所有live-out变量 (不包括他自己,假如是move指令的话,也不包括src)之间添加一个连接。遍历完该block之后我们就得到了如下的一个igraph,可以看到t175和t171之间有一条连线,而和t174之间没有连线,意味着t175和t174可以被映射到同一个寄存器 (因为他们两个的生存周期没有相互重叠),但是和t171不行。

在这里插入图片描述

注意:CFG是一个有向图,而interference graph是一个无向图

Register Allocation

  • 输入输出:Interference Graph -> "Register Allocation" -> Allocation Map
  • 作用:根据Liveness analysis生成的相交图,判断每一个寄存器是否溢出 (spill),若溢出则在stack frame上面分配一个地址,否则分配一个真正的寄存器
  • 介绍:该阶段主要负责三个功能
    • 寄存器分配 (register allocation) — 这是最基础也是最主要的功能,作用是将前面阶段产生的infinite registers MIPS转换为实际的合法MIPS程序,这就需要将其中的所有寄存器,映射到机器上面的实际寄存器。注意只要两个变量不是同时存在 (live),那么它们可以被保存到同一个寄存器当中,而这个live的信息,我们已经在liveness analysis里面分析过;
    • “合并 (coalesce)” — 前面的阶段为了方便起见,我们会产生很多的move指令,但其实很多move指令是可以去掉的,比如两条指令addi t100, $zero, 5 & move $v0, t100其实可以合并为一条addi $v0, $zer0, 5从而去掉多余的move指令
    • “溢出 (spill)” — 一些很复杂的程序需要的寄存器数量可能会超过机器拥有的数量,这种情况我们称之为"溢出 (spill)",此时需要将程序的某些变量值保存到内存里面,而不是寄存器里面,同时改写源程序
      • 在每条使用该变量的指令前面插入一条lw指令
      • 在每条写入改变量的指令后面插入一条sw指令
      • 改写完成之后,重新进行寄存器分配
  • 相关知识点:图着色 (Graph Coloring)

图着色 (K-coloring)问题就是:给定一个无向图和一定数量的颜色 (K个),问是否能够给图里的每一个节点分配一个颜色,使得每个节点与其所有相邻节点的颜色各不相同

具体到register allocation的整体实现流程如下图所示:

在这里插入图片描述

以下为每个阶段的作用和介绍:

首先定义一些术语:

  • trivial — 即一个数 (可以是degree,也可以是数量),小于颜色的数量 (亦即目标架构可以使用的寄存器数目)
  • significanttrivial的对应,数大于颜色的数量
  • spill — “溢出”,亦即需要将一个变量保存到frame里面而不是register里面
  • igraph — interference graph, 相交图
  • color — 图着色里面的颜色,对应到这里就是具体寄存器的名字 (字符串)

下面是具体流程分析:

  • build — 即利用liveness analysis,产生一个igraph,并设置好每个node的相关属性
    • interference graph的每个节点会有两个属性:
      • isPreColored — 目标架构的所有寄存器都是pre-color的,如MIPS的32个寄存器 (caller-save, callee-save, arguments, return value etc.)
      • isMoved — 该节点是否和任意move指令相关
  • simplify — 将igraph中所有"trivial degree"的节点移除,并放到stack里面(保存相邻信息)
    • trivial degree指的是该节点的连线数量 (degree)小于颜色的数量
    • 之所以可以这样做是因为,假如一个节点的degree小于颜色的数量 (即相邻节点数量小于可用颜色的数量),那么我们只要能成功color其相邻节点,那么该节点一定可以被成功color
    • 注意这里不包括所有isPreColoredisMovedtrue的节点
      • simplify的意义是我们暂时还不知道该节点是什么颜色,后面会分配颜色,但是所有的机器寄存器已经有了固定的名字/颜色,我们不可以重命名,所以pre-colored的节点都不能simplify;
      • move相关的节点我们想尽可能的将他们合并成一个节点,而不是分开单独上色,所以暂时也不能simplify
    • simplify阶段不断的循环直到没有节点可以simplify (因为我们remove掉一个节点后,所有相邻节点的degree都会减一,即可能有更多的节点可以simplify,所以需要不断的循环),simplify阶段结束后:
      • 如果只剩下pre-colored节点,那么第一阶段完成,跳转到rebuild阶段
      • 如果还有除了pre-colored节点之外的节点剩余,那么跳转到coalesce阶段,尝试合并move节点
  • coalesce — 尝试将两个move相关的节点合并成一个,合并的意思就是这两个节点可以是同一个颜色 (如如果我们发现move t100, t101这个指令是多余的,那么就可以把t100和t101都分配到$t0,然后去掉这个move指令)
    • 合并两个节点需要十分小心,不然可能会不经意间引入额外的"溢出",比如两个节点各自只有20个相邻节点,是可以正常color的,但是合并之后有40个相邻节点导致不能color从而溢出,而从性能的角度出发,我们希望能尽可能的避免"溢出"情况的出现 (起码避免由于我们的实现引起的额外"溢出")。判断两个节点合并后会不会引入额外的"溢出"是一个NP complete问题,但是我们有两个保守的heuristic可以帮忙进行判断;保守的意思就是说,假如我们说可以合并那么合并一定是安全的 (不会引入额外的"溢出");假如我们说不可以合并,则合并不一定是不安全的。
      • Brigs: 如果节点a和b合并后的ab节点,拥有trivial个significant degree的相邻节点,那么a & b可以进行合并;因为所有trivial degree的相邻节点都可以被simplify掉,然后剩下的节点数目比可用颜色数目少,所以保证一定可以color合并后的节点;
      • George: 如果节点a的所有相邻节点,要么1) 也和节点b相邻,或者2) 是trivial degree的;那么节点a & b可以进行合并;因为合并后所有trivial degree的相邻节点可以被simplify掉,剩余节点本身就与b相邻,所以保证不会引入新的"溢出";
      • 注意这两个的描述,Brigs是无顺序的,即Brigs(a, b) = Brigs(b, a),但是George是有顺序的,即George(a, b) ?= George(b, a);而只要这两个heuristic有任意一个返回true,那么我们就可以合并a,b两个节点
    • 同时注意,有两种情况是一定不能合并的:
      • 两个pre-colored的节点,因为两个机器寄存器不可能被合并成一个
      • 两个节点间本来就有一条连线,亦即两个节点彼此interfere
    • coalesce阶段,只要合并了任意两个节点,就返回simplify阶段 (因为合并后可能有更多的其他节点可以simplify);假如没有任何两个节点可以合并,则进到下一个阶段,unfreeze
  • unfreeze — 选择任意一个move edge,去掉它
    • 进入这个阶段,整个程序处于一种freeze的状态 — 既没有节点可以简化,也没有节点可以合并;此时我们就任意选取一个move edge,将其去掉 (其含义就是,这两个节点不可能被映射到同一个寄存器上)。
    • 一旦去掉任意一个move edge之后,就返回simplify阶段;但假如该阶段发现已经没有move edge了,那么就到下一个阶段,potential spill。
  • potential spill — 选择"任意"一个节点,将其去掉(放到stack里面)并标记为potential spill
    • 进入这个阶段就说明程序可能较为复杂,可能需要用到的寄存器数量比实际可用的更多,所以我们选取一个节点,将其标记为potential spill
    • 注意几点:
      • 这里说"任意"选取,其实也不是完全"任意",我们更希望选择一个访问次数较少的变量,而不是一个较多的 (比如我们会希望选取一个和循环无关的变量),因为每一次读写都会涉及到内存操作,会降低程序速度
      • 这里说的是potential,因为现阶段实际并不确定该节点是否一定会spill (要到rebuild阶段才能最后确定)
    • potential spill阶段去掉选取的节点后,返回simplify阶段继续尝试简化igraph
  • rebuild — 根据stack里面的变量,重建整个相交图,并给每一个节点上色
    • 从stack里面逐个节点pop出来,并重新加到igraph里面去,同时根据所有相邻节点的颜色,给该节点分配一个不同的颜色
    • 正常情况下,我们可以重复该过程直到给每个节点都分配了一个颜色,但假如遇到一个节点无法分配颜色 (即相邻节点已经使用完了所有可用颜色),那么就到actual spill阶段
    • 重构完整个igraph之后,假如存在没有颜色的节点 (spill),那么就到rewrite阶段;反之,register allocation完成
  • actual spill — 将该节点标记为spill (不是potential了)
    • 将不能color的节点标记为spill之后 (亦即没有颜色),返回rebuild阶段,继续color剩余节点
  • rewrite — 重写整个程序
    • 假设节点t100 (寄存器t100)被标记为spill,那么在每个读取了t100寄存器的指令前面加入一个lw指令,在每个写入了t100寄存器的指令后面加入一个sw指令
    • 重写完程序后,重新回到build阶段,重跑整个流程
拓展

整个register allocation包括liveness analysis我们做的都是intraprocedure的,亦即是对每一个函数单独做allocation;与之对应的还有一种叫做interprocedure register allocation,这个就是对整个程序,所有函数一起做register allocation。

相比较而言,interprocedure出来的结果会更好,但是实现起来也会更加的复杂。两者间一个主要的区别在于,intraprocedure的话由于每个函数彼此独立,并不知道其他函数的具体实现,亦即是说并不知道其他函数会用到哪些寄存器,为此的解决方案为calling convention,亦即每次调用一个函数,默认该函数会修改所有的a (argument), t (caller-save), v (return value)寄存器,所以假如一个变量的生存周期需要横跨函数调用的话,我们希望将其保存在s (callee-save)寄存器 (而不是t寄存器)里面;
但是interprocedure的话,由于是全局分配,我们可以知道每个函数具体使用了哪些寄存器 (这时候不再有calling convention了,t, s寄存器甚至a寄存器都已经没区别了),假如调用一个函数,并且知道它只会写入t0寄存器,那么就可以将变量保存到t1寄存器里面,因为我们知道t1寄存器肯定不会被修改。因为在interprocedure里面,每个变量相对会和更少的机器寄存器相交,所以可能一个同一个程序,使用intraprocedure分配,会涉及变量溢出,但使用interprocedure分配就不会的情况。

Code Emission

在上一阶段,我们已经成功获得了一个map,其中key为程序 (infinite registers MIPS)里面使用到的所有寄存器,value为映射到的实际寄存器值,如 t100 -> $a0。在这个阶段我们就只需要做一个简单的替换即可,将所有t100换为$a0,同时对move指令检查一下src和dst是否被映射到了同一个机器寄存器,假如是的话,直接去掉该条move指令。


至此,整个编译器各个组件的作用和部分原理就已经介绍完毕了,本篇文章由于讲的是偏原理性的东西,会比较抽象,下一篇会用一个简单的程序作为例子,带大家一起看一看编译器每个阶段的实际输出是长什么样的,看看一个程序是如何从高级语言一步步被翻译成汇编语言的。

  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值