编译原理复习

编译原理复习

语法制导翻译

  • 加上下标以区分同一类型的不同符号

属性分类

  • 综合属性 通过N的子结点或N本身的属性值来定义
  • 继承属性 依赖于N的父结点、N本身和N的兄弟结点上的属性值

S-SDD

只包含综合属性的SDD称为S属性的SDD,S属性的SDD一定可以按照自底向上的方式求值

没有副作用的SDD称为属性文法

使用依赖图来表示计算顺序,若依赖图中无环,则存在一个拓扑排序,它就是属性值的计算顺序

L-属性的SDD

语义规则中的每个属性可以是

  • 综合属性
  • 继承属性,但是继承属性仅依赖于产生式中Xj的左边符号X1, X2 , …, Xj-1的属性,和A的继承属性

依赖图的边:

  • 继承属性从左到右,从上到下。
  • 综合属性从下到上

每一个S-属性的SDD都是L-属性的SDD。

在调用之前就可以计算子节点的继承属性,调用的最后计算当前的综合属性

抽象语法树

用继承属性、综合属性来传递node,(继承属性存放左边所有非终结符合起来的node)底层T.leaf = new Leaf(num),上层E'.syn = E'.inh

语法制导的翻译方案 (SDT)

把SDD的语义规则改写为计算属性值的程序片段用花括号{ }括起来,插入到产生式右部的任何合适的位置上

产生式右边的符号的继承属性必须在这个符号以前的动作中计算出来

  • 后缀翻译方案 文法可以自底向上分析且SDD是S属性的, 必然可以构造出后缀SDT(所有动作都在产生式最右端的 SDT)
  • 后缀翻译方案可以用栈实现,使用stack[top-1].val来获取值,做完把top减去非终结符个数

On-the-fly

也是递归下降,边扫描边生成

  • 存在一个主属性,且主属性是综合属性
  • 在各产生式中,主属性是通过产生式体中各个非终结符号的主属性连接(并置)得到的。同时还会连接一些其它的元素。
  • 各非终结符号的主属性的连接顺序和它在产生式体中的顺序相同

只需要在适当的时候“发出(emit)”非主属性 的元素,即把这些元素拼接到适当的地方

L属性的自底向上语法分析

由于自底向上的归约机制,对于A的规则中的语义动作a,需要引入标记非终结符号M,M→ε{a’} ,其中a’的构造方法如下:

  • 将a中需要的A或者其它属性作为M的继承属性进行拷贝
  • 按照a中的规则计算各个属性,作为M的综合属性
A → M B C
M → ε {M.i=A.i;M.s=f(M.i);}

需要在分析栈中找到A.i,和M之间隔了一个K

L属性的自底向上语法分析代码实现

总之需要继承属性的时候,就开一个值为ε的非终结符M,然后用栈访问需要的元素

为什么需要新开M:可能无法直接知道继承属性值在分析栈中的位置

S → aA {C.i = A.s} C
S → bAB {M.i = A.s} M {C.i = M.s} C
M → ε {M.s = M.i}
C → c {C.s = g(C.i)}

其中,C出现了两次,而A的相对位置不一样

中间代码生成

抽象语法树、DAG

DAG构造:如果存在相同结构的结点,则返回已有的,否则构造新结点。

三地址代码

基本形式: x = y op z

特殊形式:

  1. x = op y,op是一元运算符, 如一元减、逻辑非等

  2. x = y 复制指令

  3. goto L 转移到L处的无条件转移语句

  4. if x goto L 或 if False x goto L 仅当 x 为真(或假)时转移到L处

  5. if x ROP y goto L 条件转移语句,仅当 x ROP y 成立时转移到L处,ROP是关系运算符 <、<=、>、>= 等

  6. param x1
    param x2
    ….
    param xn
    call p, n
    

    call p, n 表示调用过程p并且有n个实在参数

  7. x= y[z] 表示把数组元素的值赋给x; y和z分 别表示数组元素地址的不变部分和可变部分

  8. x[y]=z 表示把z的值赋给数组元素; x和y分 别表示数组元素地址的不变部分和可变部分

  9. x=&y 表示把y的地址赋给x

  10. x=*y 表示把y值为地址的存储空间的值赋 给x

  11. *x=y 表示把y值赋给x值为地址的存储空

三地址代码的具体实现

  1. 四元式 op arg1 arg2 result

    如 x=y+z : + y z x

    单目运算符不使用arg2

    param运算不使用arg2和result

    条件转移/非条件转移将目标标号放在result字段

  2. 三元式(triple) op arg1 arg2

    使用三元式的位置来引用三元式的运算结果

    x[i] = y 需要拆分为两个三元式:先求x[i]的地址,然后再赋值

    x=y op z 也需要拆分,可以引用语句编号,如:

    (0) uminus c
    (1) * b (0)
    
  3. 间接三元式表示

    用一个单独的列表表示三元式的执行顺序,语句的移动仅改变左边的语句表

    也就是单独开一个表,映射到另一个真正有代码的表,映射过程如:

    (0) (10)
    (1) (11)
    

不同表示方法的对比

  • 四元式需要利用较多的临时单元, 四元式之间的联系通过临时变量实现;
  • 中间代码优化处理时,四元式比三元式更为方便;
  • 间接三元式与四元式同样方便,两种实现方式需要的存储空间大体相同。

静态单赋值(SSA)

SSA(Static Single Assignment)中的所有赋值都针对不同名的变量,对于同一个变量在不同路径中定值的情况, 可以使用φ函数来合并不同的定值

类型表达式

基本类型:boolean, char, integer, float, etc.

  • 类名
  • 数组类型: array
  • 记录(结构)类型: record
  • 函数类型 →: 从s到t的函数表示为 s→t
  • 笛卡尔积(×):列表或元组(例如函数参数)
  • 指针类型
  • 类型表达式的变量

如:

struct {
    int no;
    char name[20];
}

类型表达式为record ((no×integer)×(name×array (20, char)))

类型等价:两个类型的值集合相等并且作用 于其上的运算集合相等。类型等价主要可以分为两种:

  • 按名字等价:两个类型名字相同,或者被定义成等价的两个名字(如typedef int ingeter)
  • 按结构等价:两个类型的结构完全相同,但是名 字不一定相同

记录(类)中字段的处理

在'{'之后把top和offset存入两个栈中,并且top置为新的环境 new Env(); offset置为0,'}'之后还原回来,并且根据top构造record的类型

生成表达式代码的SDD

  • 属性code表示代码
  • addr表示存放表达式结果的地址(临时变量)
  • top.get(…)从栈顶符号表开始, 逐个向下寻找id 的信息
  • new Temp()可以生成一个临时变量
  • gen(…)生成一 个指令

控制流语句的SDD

  • B.true:B为真的跳转目标
  • B.false:B为 假的跳转目标
  • S.next:S执 行完毕时的跳 转目标

回填

先不确定跳转的位置,记录在truelist, falselist, nextlist里,之后再填写

  • makelist(i):构造一个列表
  • merge(p1,p2):合并两个列表
  • backpatch(p,i):用i回填p指向的语句列表中的跳转语句的跳转地址
  • 声明M->ε来创建新的label: M->ε

Break、Continue的处理

生成一个跳转指令坯 ,将这个指令坯的位置加入到S的nextlist中(在符号表中设置指向S的nextlist的指针)

运行时环境

运行时环境的主要作用是实现存储组织和过程抽象,运行时环境需要考虑源语言本身的特性;

体系结构和操作系统提供了非常底层的操作,运行时环境用这些操作来实现数据存储和过程调用

  • 纯静态存储分配

    所有分配决定都在编译时得到,

    优点:不需要运行时的支持

    缺点:不支持递归调用过程,不能动态建立数据结构

    如Fortran,支持存储分配的分时复用

活动记录的结构

保存的机器状态:此次调用之前的机器状态信息,包括返回地址

局部数据:过程中声明和使用的局部变量

临时变量:中间代码或目标代码生成时产生的临时值

控制链:指向调用者的活动记录

访问链:指向过程中要访问的非局部数据所在的活动记录

栈帧中的变长局部数据

可以设计 ARP 为指向活动记录中固定长度数据的末端,TOP是栈顶指针(rsp)

恢复ARP和TOP:

  • ARP:读取控制链存放的指针
  • TOP:把(未恢复的) ARP 与被调用过程的固定长度字段的长度相加

非局部数据的访问

一个过程访问在另一个过程中声明的局部变量

  1. 静态作用域,也称词法(lexical)作用域

    非局部名字的绑定在过程被定义时决定

  2. 动态作用域

    非局部名字的绑定在过程被调用时决定

访问链的建立

如果嵌套深度为m的过程q调用嵌套深度为n的过程p:

  1. m<n

    此时一定有m+1=n,也就是p直接定义在q中

  2. m>=n

    此时一定有p的父亲是q的祖先,否则无法访问,也就是lca在深度n-1处

    那么追踪q的访问链m-n+1步,到达直接包含p的过程r的最近的活动记录,将p的访问链指向这个r的活动记录

显示表法

  • 运行时环境维护一个数组 ,为每个嵌套深度记录一个指针,指针d[i]指向最近的嵌套深度为i的活动记录
  • 使用显示表可以提高效率,访问开销是常数
  • 使用显示表需要保存和恢复:调用嵌套深度为n的过程 时,先在p的活动记录中保存原有的d[n]的值,退出时恢复

动态作用域

  • 被调用者的非局部名字a和其调用者中使用相同的存储单元,也就是不用查lca确定是哪个变量,而是调用顺序从下往上的第一个重名变量
  • 运行时环境为每个名字维护一个全局的作用域栈

过程作为参数传递

  • 传递过程作为参数时不知道上下文
  • 方法:调用者把p作为参数传递时,同时传递其访问链

将过程作为结果返回

问题:栈式管理下,访问链指向的活动记录有可能不在栈中,栈式活动记录管理不再适合

解决方法:

  1. 用堆替换栈

  2. 部分替换,在堆上分配空间存储需要的外层函数的局部数据

    lua中的机制:upvalue 初始时指向栈中数据,若逃逸(escape)则转移到堆上

调用储存和恢复运行状态的代码的分割

如果调用者做得多:生成的代码会比较长(每处调用都需要重复生成)

如果被调用者做得多:可能会有冗余的存储操作

改变可达对象集合的操作

  1. 对象分配
  2. 参数传递/返回值
  3. 引用赋值
  4. 过程返回

循环引用的垃圾回收

  • 设置弱引用

  • 或者标记-清扫垃圾回收算法的优化

    标记:从根集开始,跟踪并标记出所有可达对象

    清扫:遍历整个堆区,释放不可达对象

    优化:记录所有分配的对象,这样不需要清扫的时候遍历整个堆区

    优点:

    1. 基本没有空间代价(一个内存块只需要若干个二进制位)
    2. 可以正确处理循环数据结构

    缺点:

    1. 应用程序必须全面停顿,不适用于实时系统
    2. 可以采用增量式回收或部分回收来改善(参见第 7.7 节)
    3. 可能会造成堆区的碎片化,可以用「标记并压缩」来解决

拷贝回收器

在From半空间里分配内存,当其填满后,开始垃圾回收

回收时,把可达对象拷贝到To半空间

这样每次分配一定是从堆尾部分配,垃圾回收不用扫描整个堆区

优点

  1. 内存单元的分配代价非常低
  2. 能够高效分配可变长度的内存单元
  3. 可以自动进行压缩:消除碎片、提高局部性

缺点

  1. 内存空间利用率低(50%),如果程序所需内存较大,可能会造成大量缺页中断 ,需要足够大的物理内存

世代垃圾回收器

把堆区分成不同的年龄区域(代表不同的世代),对比较年轻的区域进行更加频繁的垃圾回收

在一个回收周期不用跟踪所有的内存单元

周期性地对「较老」的区域进行回收

目标代码生成

手动实现call ret

  • call:

    调用者把返回地址(call、br之后的下一条指令地址保存在sp-4中:ST -4(SP), #328),然后br到调用地址

    被调用者把sp加上需要的长度

  • ret:恢复原本的栈,跳转到保存的返回地址BR *-4(SP)

控制流图中的循环

循环的定义:

  • 一个结点集合 L
  • 存在一个循环入口(loop entry) 结点,唯一的前驱可以在L之外的结点
  • 每个结点都有到达入口结点的非空路径,且该路径都在L中

划分基本块

  • 每条首指令对应一个基本块:从首指令开始到下一个首指令
  • 首指令:
    1. 第一条三地址指令
    2. 跳转指令的目标指令
    3. 跳转指令之后的指令
  • 可以额外添加入口(entry)和出口(exit)结点 ,不包含指令

活跃变量分析

算法:

  • 从基本块B的最后一个语句开始反向扫描
  • 对于每个形如 x = y op z 的语句 i,依次做如下处理:
    • 把 x,y,z 当前的活跃性信息关联到语句 i
    • 设置 x 为「不活跃」
    • 设置 y 和 z 为「活跃」
  • 顺序不能反,可能有x = y 或 z,此时不是定义语句
  • 基本块出口处的活跃变量由其后继结点的入口活跃变量决定(并集)

全局寄存器分配

  • 两个不同时活跃的变量可以使用同一个寄存器
  • 可以通过对变量进行溢出操作来改变变量的活跃性

图着色方法

  • 两趟处理

  • 构造寄存器冲突图:R1在R2被定值(之后)的地方是活跃的,则它们相互冲突

  • 如果冲突图中每个结点的度数都 < m,则总是可以m-1着色 ❖ 每个结点邻居的颜色最多m-1种,总能对其着色

    方法:

    • 寻找度数<m的结点,从图中删除,并把该结点压到一个栈中
    • 找不到时进行溢出操作,删除节点
    • 图为空之后,依次弹出栈中节点,进行着色
  • 为溢出结点生成代码,使用时加载到新的符号寄存器中,然后对新的代码重新进行活跃性分析和寄存器分配

拆分(splitting)

对一个结点的活跃范围进行拆分,降低它在冲突图中的度数,使得拆分后的图可能可以进行着色

合并(coalescing)

问题:合并会增加冲突边的数目,可能导致无法着色,注意合并时不要创建高度数的结点

消除局部公共子表达式

建立某个结点之前,首先检查是否存在一个结点有相同的运算符和孩子结点(顺序也相同)

注意别名(alias)关系,指针解引用/数组取值

消除死代码

在 DAG 上消除没有附加活跃变量的根结点,重复这一处理过程直到没有结点能再被消除

窥孔优化

  • 消除冗余指令: 多余的 LD 和 ST 指令 / 不可达代码
  • 控制流优化: 控制流化简
  • 代数化简: load立即数到寄存器再用,直接用立即数更好
  • 机器特有指令的使用

数据流分析及优化

上面也有一部分内容

循环不变式代码外提

循环不变式:不管循环执多少次都得到相同结果的表达式

强度消减

归纳变量(induction variable):循环中每次x被赋值时总是增加一个常数,则称x是该循环的个归纳变量

处理循环时,按照“从里到外”的方式(先处理内层循环)

归纳变量消除

如果一组归纳变量的变化步调一致,则可以考虑消除一些

函数内连

函数内联是将代码中的函数调用直接替换为等价代码片段的一种变换方法。这非常适用于计算量不大的极简函数的优化。

控制结点

在一个流图中,如果从流图首结点到结点n的每一 条路径都包含结点d,则称d是n的控制结点,或称 结点d控制结点n

是偏序关系

对于首结点n0

  • D(n0 ) =

对于非首结点n

  • D(n) = {n}∪ ∩D(p), p∈ P(n)

其中P(n)代表结点n的所有前驱结点组成的集合

回边(Back Edge)

定义:如果n→d是流图中一条边,并且d是n的 控制结点,即d DOM n,则称n → d为回边。

一个流图是可归约的(reducible),是 指去掉其所有回边后,它不再有回路

数据流分析

程序点(program point):每条语句对应其前、后两个程序点

把每个程序点和一个数据流值(data-ow value)关联起来, 所有可能得数据流值构成的集合成为该分析的域(domain)

对每个基本块 B,记录in和out

变量符号分析

前向分析

正/负/0/槑

交汇运算:相同则取一个,否则取值为槑

循环:解方程,引入T顶值,表示无信息,循环更新直到没有变化

活跃变量分析

后向分析

经过一个语句时,去掉定义的,并上使用的

语句块的use:((((use s3 − def s2 ) ∪ use s2 ) − def s1 ) ∪ use s1 )

交汇时运算取并,在任意后继中活跃则认为是活跃的

循环:顶值为空集,循环更新直到没有变化

到达定值分析

传播的集合是语句编号的集合(d1,d2,...而非i,j,...)

前向分析

用途:常量折叠

经过一个语句时,去掉“杀死”常量的(任意赋值)kill(s) (若s对x赋值,则“杀死”了所有其它的对x的定值),并上定义常量的 gen(s)

交汇时运算取并,从任意前驱可达则认为是可达的

可用表达式分析

前向分析

到达某个程序点,每条路径一定都求了一个表达式的值,且没有更改用到的变量,则称为可用表达式

经过一条语句,如果对x赋值,则“杀死”所有用到x的表达式

交汇时运算取,要求任意前驱中都要可用才认为可用

顶值:全集(取交)

posted @ 2025-06-15 22:59  lcyfrog  阅读(36)  评论(0)    收藏  举报