编译原理:引论(二):构建一个编译器的相关科学、编译技术的应用

本文为《编译原理》(龙书) 的读书笔记 (1.4~1.5)

构建一个编译器的相关科学

编译器设计和实现中的建模

最基本的数学模型是我们将在第3章介绍的有穷状态自动机正则表达式。这些模型可以用于描述程序的词法单位(关键字、标识符等)以及描述被编译器用来识别这些单位的算法

最基本的模型中还包括上下文无关文法,它用于描述程序设计语言的语法结构,比如嵌套的括号和控制结构。我们将在第4章研究文法。类似地,树形结构是表示程序结构以及程序到目标代码的翻译方法的重要模型。我们将在第5章介绍这一概念

代码优化的科学

人们已经围绕代码优化建立了一套广泛且有用的理论。应用严格的数学基础,使得我们可以证明一个优化是正确的,并且它对所有可能的输入都产生预期的效果。从第9章开始,我们将会看到,如果想使得编译器产生经过良好优化的代码,图、矩阵和线性规划之类的模型是必不可少的。

编译器优化必须满足下面的设计目标:

  • 优化必须是正确的,也就是说,不能改变被编译程序的含义
  • 优化必须能够改善很多程序的性能
  • 优化所需的时间必须保待在合理的范围内
    我们需要使编译时间保持在较短的范围内,以支持快速的开发和调试周期。开始时, 一个程序经常在没有进行优化的情况下开发和调试。这么做不仅可以降低编译时间,更重要的是未经优化的程序比较容易调试。这是因为编译器引入的优化经常使得源代码和目标代码之间的关系变得模糊。在编译器中开启优化有时会暴露出源程序中的新问题,因此需要对经过优化的代码再次进行测试。因为可能需要额外的测试下作,有时会阻止人们在应用中使用优化技术,当应用的性能不很重要的时候更是如此
  • 所需要的工程方面的工作必须是可管理的

编译技术的应用

高级程序设计语言的实现

一个高级程序设计语言定义了一个编程抽象:程序员使用这个语言表达算法,而编译器必须把这个程序翻译成目标语言。总的来说,用高级程序设计语言编程比较容易,但是比较低效,也就是说,目标程序运行较慢。使用低级程序设计语言的程序员能够更多地控制一个计算过程,因此从原则上讲,可以产生更加高效的代码。遗憾的是,低级程序比较难编写,而且更糟糕的是可移植性较差,更容易出错,而且更加难以维护。优化编译器包括了提高所生成代码性能的技术,因此弥补了因高层次抽象而引人的低效率

例1.2
C语言中的关键字register是编译器技术和语言发展互动的一个较早的例子。当C语言在20世纪70年代中期被创立时,人们认为有必要让程序员来控制哪个程序变量应该存放在寄存器中。当有效的寄存器分配技术出现后,这个控制变得没有必要了,大多数现代的程序不再使用这个语言特征。
实际上,使用关键字register的程序还可能损失效率,因为寄存器分配是一类很低层次的问题,程序员常常不是最好的判断这类问题的人选。寄存器分配的最优选择很大程度上取决于一个机器的体系结构的特点。把低层次资源管理的决策,比如寄存器分配,写死在程序中反而有可能损害性能。当运行程序的计算机有别于当初所设定的目标机时更是如此。

在实践中,所有的通用程序设计语言,包括C、Fortran, 都支持用户定义的聚合类型(如数组和结构)和高级控制流(比如循环和过程调用)。如果我们仅仅把每个高级结构和数据存取运算直接翻译成为机器代码,得到的代码将会非常低效。编译器优化的一个组成部分称为数据流优化,它可以对程序的数据流进行分析,并消除这些构造之间的冗余。它们很有效,生成的代码和一个熟练的低级语言程序员所写的代码类似。

针对计算机体系结构的优化

计算机体系结构的快速发展也对新编译器技术提出了越来越多的需求。几乎所有的高性能系统都利用了两种技术:并行(parallelism)和内存层次结构(memoryhierarchy)

  • 并行可以出现在多个层次上:在指令层次上,多个运算可以被同时执行;在处理器层次上,同一个应用的多个不同线程在不同的处理器上运行
  • 内存层次结构是应对下述局限性的方法:我们可以制造非常快的内存,或者非常大的内存,但是无法制造非常大又非常快的内存

并行性

所有的现代微处理器都采用了指令级并行性。但是,这种并行性可以对程序员隐藏起来。程序员写程序的时候就好像所有指令都是顺序执行的。硬件动态地检测顺序指令流之间的依赖关系,并在可能的时候并行地发出指令

在有些情况下,机器包含一个硬件调度器。该调度器可以改变指令的顺序以提高程序的并行性。不管硬件是否对指令进行重新排序,编译器都可以重新安排指令,以使得指令级并行更加有效

指令级的并行也显式地出现在指令集中。VLIW (Very Long Instruction Word, 非常长指令字)机器拥有可并行执行多个运算的指令。Intel IA64是这种体系结构的一个有名的例子。所有的高性能通用微处理器还包含可以同时对一个向量中的所有数据进行运算的指令。人们已经开发出了相应的编译器技术,从顺序程序出发为这样的机器自动生成代码

多处理器也已经日益流行。程序员可以为多处理器编写多线程的代码,也可以通过编译器从传统的顺序程序自动生成并行代码。这样的编译器对程序员隐藏了一些细节,包括如何在程序中找到并行性,如何在机器中分发计算任务,以及如何最小化处理器之间的同步和通信

内存层次结构

一个内存层次结构由几层具有不同速度和大小的存储器组成。离处理器最近的层速度最快但是容量最小。如果一个程序的大部分内存访问都能够由层次结构中最快的层满足,那么程序的平均内存访问时间就会降低。并行性和内存层次结构的存在都会提高一个机器的潜在性能。但是它们必须被编译器有效利用才能够真正为一个应用提供高性能计算

内存层次结构可以在所有的机器中找到。一个处理器通常有少量的几百个字节的寄存器,几层包含了几K到几兆字节的高速缓存,包含了几兆到几G字节的物理寄存器,最后还包括多个几G字节的外部存储器。相应地,层次结构中相邻层次间的存取速度会有两到三个数量级上的差异。系统性能经常受到内存子系统的性能(而不是处理器的性能)的限制。虽然一般来说编译器注重优化处理器的执行,现在人们更多地强调如何使得内存层次结构更加高效

高效使用寄存器可能是优化一个程序时要处理的最重要的问题。和寄存器必须由软件明确管理不同,高速缓存和物理内存是对指令集合隐藏的,并由硬件管理。人们发现,由硬件实现的高速缓存管理策略有时并不高效。当处理具有大型数据结构(通常是数组)的科学计算代码时更是如此。我们可以改变数据的布局或数据访问代码的顺序来提高内存层次结构的效率。我们也可以通过改变代码的布局来提高指令高速缓存的效率

新计算机体系结构的设计

在现代计算机体系结构的开发中,编译器在处理器设计阶段就进行开发,然后编译得到代码并运行于模拟器上。这些代码被用来评价提议的体系结构特征

RISC

有关编译器如何影响计算机体系结构设计的最有名的例子之一是RISC (Reduced Instruction-Set Computer, 精简指令集计算机)的发明。在发明RISC之前,趋势是开发的指令集越来越复杂,以使得汇编编程变得更容易。这些体系结构称为CISC (Complex Instruction-Set Computer, 复杂指
令集计算机)。比如, CISC 指令集包含了复杂的内存寻址模式来支持对数据结构的访问,还包含了过程调用指令来保存寄存器和向栈中传递参数

编译器优化经常能够消除复杂指令之间的冗余,把这些指令削减为少量较简单的运算。因此,人们期望设计出简单指令集。编译器可以有效地使用它们,而硬件也更容易进行优化

大部分通用处理器体系结构,包括PowerPC、SPARC、MIPS、Alpha和PA-RISC, 都是基于RISC概念的。虽然x86体系结构具有CISC指令集,但在这个处理器本身的实现中使用了很多为RISC机器发展得到的思想。不仅如此,使用高性能x86机器的最有效的方法是仅使用它的简单指令

通信协议的转换

用编译原理中识别程序设计语言的标识符和数的识别原理,来进行协议帧内部成分的识别

程序翻译

二进制翻译

编译器技术可以用于把一个机器的二进制代码翻译成另一个机器的二进制代码,使得可以在一个机器上运行原本为另一个指令集编译的程序。特别地,因为x86在个人计算机市场上的主导地位,很多软件都是以x86二进制代码的形式提供的。人们开发了二进制代码翻译器,把x86代码转换成Alpha和Spare的代码

二进制翻译也可以被用来提供向后兼容性。1994年, 当Apple Macintosh中的处理器从Motorola MC68 040变为PowerPC的时候,便使用二进制翻译来支待PowerPC处理器运行遗留下来的MC68040代码

硬件合成

大部分硬件设计也是使用高级硬件描述语言(Verilog, VHDL)描述的

硬件设计通常是在寄存器传输层(Register Transfer Level, RTL)上描述的。在这个层中,变量代表寄存器,而表达式代表组合逻辑。硬件合成工具把RTL 描述自动翻译成为门电路,而门电路再被翻译成为晶体管,最后生成一个物理布局。和程序设计语言的编译器不同,这些工具经常会花费几个小时来优化门电路。还存在一些用来翻译更高层次(比如行为和函数层次)的设计描述的技术

查询解释器

数据库查询由关系和布尔运算符的谓词组成。它们可以被解释,也可以编译为代码,以便在一个数据库中搜索满足这个谓词的记录

数据库管理系统即可看成是一个编译系统

软件生产率工具

一个很有意思且很有前景的程序测试的辅助性方法是通过数据流分析技术静态地(即在程序运行之前)定位错误。数据流分析可以在所有可能的执行路径上找到错误,而不是像程序测试的时候所做的那样,仅仅是在那些由输入数据组合执行的路径上找错误。很多原本为编译器优化所开发的数据流分析技术可以用来创建相应的工具,帮助程序员完成他们的软件工程任务

找到程序的所有错误是不可判定问题。可以设计一个数据流分析方法来找出所有可能带有某种错误的语句,对程序员发出警告。但是如果这些警告中的大部分都是误报,用户将不会使用这个工具。因此,实用的错误检测器经常既不是健全的也不是完全的。也就是说,它们不可能找出程序中的所有错误,也不能保证报告的所有错误都真正是错误。虽然如此,人们仍然开发了很多种静态分析工具,这些工具能够在实际程序中有效地找到错误,比如释放空指针或已释放过的指针

在本节中,我们将提到使用程序分析技术来提高软件生产效率的几个已有途径。这些分析是在原本为编译器代码优化而开发的技术的基础上建立的。其中静态探测一个程序是否具有安全漏洞的技术是极为重要的

类型检查

通过分析程序中的数据流,程序分析还可以做出比检查类型错误更多的工作。比如, 一个指针被赋予了NULL值,然后又立刻被释放了,这个程序显然是错误的

这个技术也可以用来捕捉某种安全漏洞。其中,攻击者可以向程序提供一个字符串或者其他数据,而这些数据没有被程序谨慎使用。一个用户提供的字符串可以被加上一个“ 危险” 的标号。如果没有检查这个字符串是否满足特定的格式,那么它仍然是" 危险" 的。如果这种类型的字符串能够在某个程序点上影响代码的控制流,那么就存在一个潜在的安全漏洞

边界检查

很多系统中的安全漏洞都是因为用C语言编写的程序中的缓冲区溢出造成的。因为C语言没有数组边界检查,所以必须由用户来保证对数组的访问没有超出边界。因为不能检验用户提供的数据是否可能溢出一个缓冲区,程序可能被欺骗,把一个数据存放到缓冲区之外。攻击者可以巧妙处理这些数据,使得程序做出错误的行为,从而危及系统的安全。人们已经开发了一些技术来寻找程序中的缓冲区溢出,但收效并不显著

内存管理工具

垃圾收集机制是在效率和易编程及软件可靠性之间进行折衷处理的另一个极好的例子。自动的内存管理消除了所有的内存管理错误(比如内存泄漏)。人们开发了很多工具来帮助程序员寻找内存管理错误

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值