编译器 - 中间代码生成

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

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

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

  • 编译器的第二部分,即综合,根据目标机器而变化。

  • 通过在中间代码上应用代码优化技术,可以更轻松地应用源代码修改来提高代码性能。

中间表示

中间代码可以以多种方式表示,并且它们有自己的好处。

  • 高级 IR - 高级中间代码表示非常接近源语言本身。它们可以很容易地从源代码生成,我们可以轻松地应用代码修改来提高性能。但对于目标机器优化,它不太受欢迎。

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

中间代码可以是特定于语言的(例如,Java 的字节码),也可以是独立于语言的(三地址代码)。

三地址代码

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

例如:

a = b + c * d;

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

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

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

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

四元组

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

Op arg1 arg2 result
* c d r1
+ b r1 r2
+ r2 r1 r3
= r3 a

三元组

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

Op 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