编译器 - 中间代码生成



源代码可以直接翻译成目标机器码,那么为什么还需要将源代码翻译成中间代码,然后再翻译成目标代码呢?让我们看看为什么需要中间代码。

Intermediate Code
  • 如果编译器在没有生成中间代码选项的情况下将源语言翻译成目标机器语言,那么对于每台新机器都需要一个完整的原生编译器。

  • 中间代码通过保持所有编译器的分析部分相同,消除了为每台独特的机器都需要一个新的完整编译器的需求。

  • 编译器的第二部分,合成部分,会根据目标机器进行更改。

  • 通过在中间代码上应用代码优化技术,更容易应用源代码修改以提高代码性能。

中间表示

中间代码可以用多种方式表示,它们各有优势。

  • 高级IR - 高级中间代码表示非常接近源语言本身。它们可以很容易地从源代码生成,并且我们可以很容易地应用代码修改以增强性能。但对于目标机器优化,它不太理想。

  • 低级IR - 这个更接近目标机器,这使得它适合寄存器和内存分配、指令集选择等。它有利于机器相关的优化。

中间代码可以是特定于语言的(例如,Java的字节码)或与语言无关的(三地址码)。

三地址码

中间代码生成器从其前阶段语义分析器接收输入,形式为带注释的语法树。然后可以将该语法树转换为线性表示,例如后缀表示法。中间代码倾向于与机器无关的代码。因此,代码生成器假设有无限数量的内存存储(寄存器)来生成代码。

例如

a = b + c * d;

中间代码生成器将尝试将此表达式分解成子表达式,然后生成相应的代码。

r1 = c * d;
r2 = b + r1;
a = r2

r 用作目标程序中的寄存器。

三地址码最多有三个地址位置来计算表达式。三地址码可以用两种形式表示:四元式和三元式。

四元式

四元式表示中的每个指令都分为四个字段:运算符、arg1、arg2和结果。上面的例子在四元式格式中表示如下

操作符 arg1 arg2 结果
* c d r1
+ b r1 r2
+ r2 r1 r3
= r3 a

三元式

三元式表示中的每个指令都有三个字段:操作符、arg1和arg2。各个子表达式的结果由表达式的位 置表示。三元式在表示表达式时与DAG和语法树类似。它们在表示表达式时等同于DAG。

操作符 arg1 arg2
* c d
+ b (0)
+ (1) (0)
= (2)

三元式在优化时面临代码不可移动的问题,因为结果是位置性的,改变表达式的顺序或位置可能会导致问题。

间接三元式

这种表示是对三元式表示的增强。它使用指针而不是位置来存储结果。这使得优化器可以自由地重新定位子表达式以生成优化的代码。

声明

变量或过程必须在使用前声明。声明包括在内存中分配空间以及在符号表中输入类型和名称。程序的编写和设计可以考虑目标机器结构,但并不总是能够准确地将源代码转换为目标语言。

将整个程序视为过程和子过程的集合,就可以声明所有局部于过程的名称。内存分配以连续的方式进行,名称按照程序中声明的顺序分配给内存。我们使用偏移量变量并将它的值设置为零{offset = 0},表示基地址。

源程序语言和目标机器架构在存储名称的方式上可能有所不同,因此使用相对寻址。当第一个名称从内存位置0 {offset=0}开始分配内存时,稍后声明的下一个名称应该分配给第一个名称旁边的内存。

示例

我们以C语言为例,其中一个整型变量分配2字节内存,一个浮点型变量分配4字节内存。

int a;
float b;

Allocation process:
{offset = 0}

   int a;
   id.type = int
   id.width = 2

offset = offset + id.width 
{offset = 2}

   float b;
   id.type = float
   id.width = 4
   
offset = offset + id.width 
{offset = 6}

为了将此细节输入符号表,可以使用过程enter。此方法可能具有以下结构

enter(name, type, offset)

此过程应该在符号表中为变量name创建条目,其类型设置为type,数据区中的相对地址为offset

广告