📜  编译器设计-代码生成

📅  最后修改于: 2021-01-18 05:30:39             🧑  作者: Mango


代码生成可以视为编译的最后阶段。通过后期代码生成,可以将优化过程应用于代码,但是可以将其视为代码生成阶段本身的一部分。编译器生成的代码是某些较低级编程语言(例如汇编语言)的目标代码。我们已经看到,用高级语言编写的源代码被转换为导致较低级目标代码的较低级语言,该目标代码应具有以下最小属性:

  • 它应该带有源代码的确切含义。
  • 就CPU使用率和内存管理而言,它应该是高效的。

现在,我们将看到中间代码如何转换为目标对象代码(在这种情况下为汇编代码)。

有向无环图

有向无环图(DAG)是一种工具,它描述基本块的结构,有助于查看在基本块之间流动的值的流向,也可以提供优化。 DAG提供了对基本块的轻松转换。 DAG可以在这里理解:

  • 叶节点代表标识符,名称或常量。

  • 内部节点代表运算符。

  • 内部节点还表示要存储或分配值的表达式或标识符/名称的结果。

例:

t0 = a + b
t1 = t0 + c
d = t0 + t1
Directed Acyclic Graph

[t0 = a + b]

Directed Acyclic Graph

[t1 = t0 + c]

Directed Acyclic Graph

[d = t0 + t1]

窥孔优化

此优化技术在源代码上本地工作,以将其转换为优化的代码。在本地,我们指的是手头的代码块的一小部分。这些方法可以应用于中间代码以及目标代码。对一堆语句进行分析并检查以下可能的优化:

冗余指令消除

在源代码级别,用户可以执行以下操作:

int add_ten(int x)
   {
   int y, z;
   y = 10;
   z = x + y;
   return z;
   }
int add_ten(int x)
   {
   int y;
   y = 10;
   y = x + y;
   return y;
   }
int add_ten(int x)
   {
   int y = 10;
   return x + y;
   }
   
   
int add_ten(int x)
   {
   return x + 10;
   }
   
   
   

在编译级别,编译器搜索本质上多余的指令。即使删除了某些指令,多次加载和存储指令也可能具有相同的含义。例如:

  • MOV x,R0
  • MOV R0,R1

我们可以删除第一条指令并将句子重写为:

MOV x, R1

无法访问的代码

无法访问的代码是程序代码的一部分,由于编程结构而无法访问。程序员可能不小心编写了一段永远无法到达的代码。

例:

void add_ten(int x)
{
   return x + 10;
   printf(“value of x is %d”, x);
}

在此代码段中,将永远不会执行printf语句,因为程序控件可以在执行之前返回,因此可以删除printf

控制优化流程

在代码中有些实例中,程序控件来回跳转而不执行任何重要任务。这些跳转可以删除。考虑以下代码块:

...        
MOV R1, R2
GOTO L1
...
L1 :   GOTO L2
L2 :   INC R1

在此代码中,标签L1可以在将控件传递给L2时删除。因此,控件可以直接到达L2,而不是先跳转到L1再跳转到L2,如下所示:

...        
MOV R1, R2
GOTO L2
...
L2 :   INC R1

代数表达式简化

在某些情况下,可以简化代数表达式。例如,表达式A = A + 0可以通过本身和表达替换= A + 1可以简单地通过一个INC被替换。

强度降低

有些操作会消耗更多的时间和空间。可以通过用其他耗时少又省空间但产生相同结果的操作代替它们来降低它们的“强度”。

例如,可以将x * 2替换为x << 1 ,这仅涉及一个左移。尽管a和2的输出相同,但是2的实现效率更高。

访问机器指令

目标机器可以部署更复杂的指令,这些指令可以有效地执行特定操作。如果目标代码可以直接容纳这些指令,则不仅可以提高代码质量,而且可以产生更有效的结果。

代码生成器

期望代码生成器了解目标计算机的运行时环境及其指令集。代码生成器应该考虑以下因素来生成代码:

  • 目标语言:代码生成器必须知道要转换代码的目标语言的性质。该语言可能有助于某些特定于机器的指令,以帮助编译器以更方便的方式生成代码。目标计算机可以具有CISC或RISC处理器体系结构。

  • IR类型:中间表示形式多种多样。它可以采用抽象语法树(AST)结构,反向波兰表示法或3地址代码。

  • 指令选择:代码生成器将中间表示作为输入并将其转换(映射)为目标机器的指令集。一个表示形式可以有多种方法(指令)进行转换,因此,代码生成器有责任明智地选择适当的指令。

  • 寄存器分配:程序在执行过程中具有许多要保留的值。目标计算机的体系结构可能不允许所有值都保留在CPU内存或寄存器中。代码生成器决定将哪些值保留在寄存器中。同样,它决定用于保留这些值的寄存器。

  • 指令的顺序:最后,代码生成器确定指令的执行顺序。它为指令创建时间表以执行它们。

描述符

代码生成器在生成代码时必须同时跟踪寄存器(以确保可用性)和地址(值的位置)。对于它们两者,使用以下两个描述符:

  • 寄存器描述符:寄存器描述符用于通知代码生成器寄存器的可用性。寄存器描述符跟踪每个寄存器中存储的值。每当在代码生成过程中需要一个新的寄存器时,就使用该描述符查询寄存器的可用性。

  • 地址描述符:程序中使用的名称(标识符)的值在执行时可能存储在不同的位置。地址描述符用于跟踪存储标识符值的存储位置。这些位置可能包括CPU寄存器,堆,堆栈,内存或上述位置的组合。

代码生成器使两个描述符都实时更新。对于装入语句LD R1,x,代码生成器:

  • updates the Register Descriptor R1 that has value of x and
  • updates the Address Descriptor (x) to show that one instance of x is in R1.

代码生成

基本块由一系列三地址指令组成。代码生成器将这些指令序列作为输入。

注意:如果在多个位置(寄存器,高速缓存或内存)找到名称的值,则该寄存器的值将优先于高速缓存和主存储器。同样,缓存的值比主内存更可取。主存储器几乎没有任何优先选择。

getReg :代码生成器使用getReg函数来确定可用寄存器的状态以及名称值的位置。 getReg的工作方式如下:

  • 如果变量Y已在寄存器R中,则使用该寄存器。

  • 否则,如果某些寄存器R可用,它将使用该寄存器。

  • 否则,如果以上两个选项都不可行,则选择需要最少数量的加载和存储指令的寄存器。

对于指令x = y OP z,代码生成器可以执行以下操作。让我们假设L是要保存y OP z的输出的位置(最好是寄存器):

  • 调用函数getReg来确定L的位置。

  • 通过查询y的地址描述符来确定y的当前位置(寄存器或存储器)。如果y目前不在寄存器L中,则生成以下指令以将y的值复制到L中

    MOV y’,L

    其中y”代表y的复制值。

  • 使用步骤2中用于y的相同方法确定z的当前位置,并生成以下指令:

    OP z’,L

    其中,z”表示z的复制值。

  • 现在L包含y OP z的值,该值打算分配给x 。因此,如果L是一个寄存器,请更新其描述符以指示它包含x的值。更新x的描述符以指示它存储在位置L。

  • 如果y和z不再使用,则可以将其返回给系统。

诸如循环和条件语句之类的其他代码结构以常规汇编方式转换为汇编语言。