三地址码简介

三地址码简介

三地址码(Three Address Code)是一种最常用的中间语言,编译器可以通过它来改进代码转换效率。每个三地址码指令,都可以被分解为一个四元组(4-tuple)的形式:(运算符,操作数1,操作数2,结果)。由于每个陈述都包含了三个变量,即每条指令最多有三个操作数,所以它被称为三地址码。

编译器

编译器(compiler),是一种计算机程序,它会将用某种编程语言写成的源代码(原始语言),转换成另一种编程语言(目标语言)。

它主要的目的是将便于人编写、阅读、维护的高级计算机语言所写作的源代码程序,翻译为计算机能解读、运行的低阶机器语言的程序,也就是可执行文件。编译器将原始程序(source program)作为输入,翻译产生使用目标语言(target language)的等价程序。源代码一般为高级语言(High-level language),如Pascal、C、C++、C# 、Java等,而目标语言则是汇编语言或目标机器的目标代码(Object code),有时也称作机器代码(Machine code)。

一个现代编译器的主要工作流程要通常要经过预处理、编译、汇编、链接等步骤。关于编译器的工作流程的介绍,可参考:从C源代码到可执行文件的四个过程:预处理、编译、汇编、链接

中间语言

中间语言(Intermediate language),有时也称为中间表示(Intermediate Rrepresentation,IR)在计算机科学中,是指一种应用于抽象机器(abstract machine)的编程语言,它设计的目的,是用来帮助我们分析计算机程序。这个术语源自于编译器,在编译器将源代码编译为目的码的过程中,会先将源代码转换为一个或多个的中间表述,以方便编译器进行最佳化,并最终产生出目的机器的机器语言。通常,中间语言的设计与一般的机器语言有三个不同之处:

  • 每个指令代表仅有一个基本的操作。举例来说,在微处理器中出现的 shift-add 定址模式在中间语言不会出现。
  • 指令集内可能不会包含控制流程的资讯。
  • 暂存器可用的数量可能会很大,甚至没有限制。

最常见的中间语言表述形式,是三位址码(Three address code),常简称为 TAC 或 3AC。

这个术语也同时用来代称一些作为中间层的语言,有些高级语言不会输出为机器语言,它们仅会输出这种中间语言,而这些中间语言则会像一般语言一样,提交给编译器,编译为机器语言。这通常被用于让最佳化的过程更简单,也用于增进可移植性的能力,改进移植的方式则是利用中间语言的编译器,可以编译出许多中央处理器及操作系统可使用的机器码,例如C语言。中间语言的复杂度,通常介于高阶语言及低级语言之间,例如汇编语言。

四元式

四元式主要由四部分组成:OP,arg1,arg2,result,即(操作符,操作数1,操作数2,结果),

其中,OP是运算符,arg1,arg2分别是第一和第二个运算对象,result是编译程序为存放中间运算结果而引进的变量,常称为临时变量。当OP是一目运算时,常常将运算对象定义为arg1。

例如 X = a*b+c/d 的四元式序列:

  1. (*, a, b, T1)
  2. (/, c, d, T2)
  3. (+, T1, T2, T3)
  4. (=, T3, -, X)
  • 四元式出现的顺序和语法成份的计值顺序相一致。
  • 四元式之间的联系是通过临时变量实现的,这样易于调整和变动四元式。
  • 便于优化处理。

三地址码

三地址代码是四元式的另一种表示形式。每个三地址码指令,都可以被分解为一个四元式(4-tuple)。因为每个陈述都包含了三个变量,所以它被称为三地址码。

上面例子 X = a*b+c/d 的三地址序列:

  1. t1=a*b
  2. t2=c/d
  3. t3=t1+t2
  4. X=t3

常用的三地址码(三地址码形式和四元组形式)

序号指令类型指令形式备注
1赋值指令x = y op zx = op yop为运算符
2复制指令x = y
3条件跳转if x relop y goto nrelop为关系运算符
4非条件跳转goto n跳转到地址n的指令
5参数传递param x将x设置为参数
6过程调用call p,np为过程的名字n为过程的参数的个数
7过程返回return x
8数组引用x=y[i]i为数组的偏移地址,而不是下标
9数组赋值x[i]=y
10地址及指针操作x=&yx=*y *x=y

将上表的三地址指令用四元式表示

x = y op z( op , y , z , x)
x = op y( op , y , _ , x)
x = y( = , y , _ , x)
if x relop y goto n( relop , x , y , n)
goto n( goto , _ , _ , n)
param x( param , _ , _ , x)
call p,n( call , p , n , _)
return x( return , _ , _ , x)
x=y[i]( =[] , y , i , x) ps: y为基地址,i为偏移地址
x[i]=y( []= , y , x , i)
x=&y( & , y , _ , x)
x=*y( =* , y , _ , x)
*x=y( *= , y , _ , x)

每一个指令只有一个操作符,那么只完成一个动作,这样看来,三地址指令序列唯一确定了运算完成的顺序。

中间代码生成的例子

while a<b do
   if c<5 then
      while x>y do
          z=x+1;
   else x=y;

100到112为指令的编码,从100到112顺序执行。

100: ( j<, a, b, 102 )如果a<b ,那么跳转到102指令,否则继续执行101指令
101: ( j , -, -, 112 )该指令为无条件指令,跳转到112
102: ( j<, c, 5, 104 )如果c<5 ,那么跳转到104指令,否则继续执行103指令
103: ( j , -, – , 110 )该指令为无条件指令,跳转到110
104: ( j>, x, y, 106 )如果x>y ,那么跳转到106指令,否则继续执行105指令
105: ( j , -, – , 100 )该指令为无条件指令,跳转到100
106: ( + , x, 1 , t1 )x+1的值赋值给t1
107: ( = , t1, – , z )t1的值赋值给z,106和107完成了一条语句
108: ( j , -, – , 104 )该指令为无条件指令,跳转到104
109: ( j , -, – , 100 )该指令为无条件指令,跳转到100
110: (= , y, – , x )把y赋值给x,然后执行111指令
111: ( j , -, – , 100 )该指令为无条件指令,跳转到100
112:结束

中间表示IR的演变历史

计算机科学家提出三地址代码的理由如下:三地址代码是一种线性IR。由于输入源程序及输出目标程序都是线性的,因此,线性IR有着其他形式无法比拟的优势。另外,相对于其他表示形式而言,程序员对于线性表示形式通常会有一种莫名的亲切感,编译器设计者当然也不例外。早期编译器设计者往往都是汇编语言程序设计的高手,可以非常自然、流畅地阅读线性的三地址代码形式。同时,线性表示形式也会降低输入输出的实现难度。随着编译器”端”、”遍”等概念的出现,IR已经不仅仅是一种存储在内存中的数据结构。有时它也需要以文件形式转存输出,作为接口供其他系统读取使用。

那么,一定有读者会心存疑问:为什么将其设计为”三地址”的形式呢?实际上,这是计算机科学家经过多年实践探索后才得到共识的。三地址代码并不是唯一的线性IR,只能说是最为常见的而已。在编译技术领域,二地址代码、单地址代码(即栈式机代码)都曾出现过,也曾在某些应用领域盛行一时,尤其是单地址代码。

二地址代码比较简单,就是选择其中一个对象同时充当运算分量与目标操作数。在早期,二地址代码主要就是着眼于x86机器而提出的。不过,实践证明,这只是人们的一厢情愿而已,即使是针对x86机器,二地址的优势也并不明显,它反而可能会给编译器带来一定的麻烦,所以这种表示形式已经逐步被淘汰了。

然而,单地址代码的情况则截然不同了,在现代编译器设计中,单地址代码也是应用比较广泛的一种IR。尤其是近年随着混合语言的日渐壮大,单地址代码也重新进入了人们的视野。由于执行单地址代码程序的栈式机架构相对比较简单,可以非常方便地构造相关的解释器或虚拟机,所以单地址代码深受混合语言设计者的欢迎。读者熟悉的Java字节码、.NET的IL都是单地址代码。栈式机或者单地址代码与常见的x86体系结构相差甚远,可能读者所知不多。不过,单地址代码还是一种比较有意思的表示形式,因此,笔者想通过一个简单的实例让读者对单地址代码有所了解。

三地址代码是在二地址代码的基础上发展而来的。二地址代码的不足之处在于它通常会给其中一个源操作分量带来一定副作用。当然,这种设计的灵感最初是来源于x86指令系统的,但是却忘了一个重要的区别:x86指令中往往都是以寄存器作为暂存空间的。而暂存空间对于二地址代码却是一个棘手的问题。为了解决二地址代码的不足,人们提出了一个对源操作分量不产生任何副作用的形式,那就是三地址代码。也就是说,在一行三地址代码中,任何运算都不会改变两个源操作分量。这是三地址代码与二地址代码的主要区别。这个特性是非常重要的,它将使得编译器更自由地复用名字与值,不必考虑代码带来的副作用。

一般来说,三地址代码的大多数操作都是由四项组成,即一个操作码和三个地址。不过,三地址代码同样存在级别差异。随着语言复杂性的提高,在现代编译器设计中,三地址代码的级别概念显得尤其重要。根据编译器设计的需要,有些三地址代码可能近似于源语言,而有些三地址代码则更接近于目标语言。当然,级别主要就是取决于三地址代码的操作符及操作分量的复杂性。下面,笔者就操作符及操作分量这两个话题来讨论三地址代码。

操作符是用于标识三地址代码操作含义的元素。根据源语言、目标语言的特点,三地址代码操作符的集合以及抽象程度是各不相同的。其中,抽象程度是三地址代码设计中的重要因素之一。一般而言,三地址代码将包含大部分低级操作,即目标机所支持的指令。不过,这并不意味着三地址代码就是机器指令系统的映射。设计者应该从便于后端处理的角度考虑,尽可能地发挥三地址代码作为中间语言的作用

Ref:

https://zh.m.wikipedia.org/wiki/%E4%B8%89%E4%BD%8D%E5%9D%80%E7%A2%BC

https://baike.baidu.com/item/%E4%B8%89%E5%9C%B0%E5%9D%80%E7%A0%81/23121007

https://blog.csdn.net/starter_____/article/details/90146048

https://jishuin.proginn.com/p/763bfbd55cbd

https://book.51cto.com/art/201206/340208.htm

    原文作者:Adenialzz
    原文地址: https://blog.csdn.net/weixin_44966641/article/details/121718233
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞