1.4 编译器和解释器

(EXAMPLE 1.7 Pure compilation)在抽象的最高层次上,高级语言程序的编译和执行如下所示:

编译器将高级语言程序转换为等效的目标程序(通常使用机器语言),用户可以在任意时刻告诉操作系统运行目标程序。编译器控制编译过程,目标程序控制执行过程。编译器是一个机器语言程序,可能是通过编译其它高级程序创建的。操作系统可以理解的机器语言文件通常被称为目标代码。

(EXAMPLE 1.8 Pure interpretation)高级语言的另一种实现方式为解释器:

与编译器不同,解释器在应用程序的执行过程中一直存在,事实上,解释器控制程序的执行过程。解释器实现了一个虚拟机,其“机器语言”是高级语言。解释器读取高级语言的语句,然后执行它们。

一般来说,解释执行比编译执行具有更好灵活性,也可以更方便的诊断错误消息。因为解释器直接执行源代码,所以它可以包含一个功能强大的源码级调试器。解释器还可以处理程序本身的特性,例如变量的大小和类型,甚至是变量的名称,这些依赖于执行的源程序。有些语言特性在没有解释器的情况下几乎不可能实现:例如,在Lisp和Prolog中,程序在执行过程中可以产生新的代码并执行它们。(一些脚本语言也提供了这种功能。)将程序实现的决策延迟到运行时称为延迟绑定,将在3.1节中讨论。

相比之下,编译执行的性能更佳。通常来说,能在编译时做出的决策不需要在运行时做出。例如,如果编译器可以保证变量x的地址始终为49378,那么它可以生成访问该地址的机器指令,源程序引用x时直接访问该位置即可。反观解释器,它可能需要在每次访问x时,在表中查找x的其地址。由于(最终版本的)程序只编译一次,执行多次,所以可以节省很多资源,而解释器则需要在循环的每次迭代中都做不必要的查找。

(EXAMPLE 1.9 Mixing compilation and interpretation)虽然编译和解释在概念上有着明显的区别,但大多数语言实现是两者的混合,通常如下所示:

如果最初的转换过程很简单,我们就说语言是“解释”的;如果转换过程很复杂,我们就说语言是“编译的”。这种区别可能会令人混淆,因为“简单”和“复杂”是主观术语,而且编译器(复杂翻译器)可能生成代码,然后由复杂的虚拟机(解释器)执行——事实上这就是Java的实现方式。如果翻译过程对语言进行了彻底的分析(而不是进行一些“机械”的转换),并且中间程序与源程序没有很强的相似性,那么这就是一种语言的编译过程。这两个特性——彻底的分析和不平凡的转换——是编译的标志。

DESIGN & IMPLEMENTATION 1.2 Compiled and interpreted languages

Certain languages (e.g., Smalltalk and Python) are sometimes referred to as “interpreted languages” because most of their semantic error checking must be performed at run time. Certain other languages (e.g., Fortran and C) are sometimes referred to as “compiled languages” because almost all of their semantic error checking can be performed statically. This terminology isn’t strictly correct: interpreters for C and Fortran can be built easily, and a compiler can generate code to perform even the most extensive dynamic semantic checks.That said, language design has a profound effect on “compilability.”

语言实现的策略包括:

  • (EXAMPLE 1.10 Preprocessing)大多数解释型语言使用初始翻译器(预处理器)来移除注释和空格,并将源代码分割为关键字、标识符、数字和符号等标记。在宏汇编器中,预处理器还会展开宏。预处理器也可以识别循环和子例程等更高级的语法结构。预处理器的目的是生成一种可以被高效解释的源代码中间表示形式。

    早期的Basic手册建议删除程序中的注释,因为这样可以提高其性能,所以这种实现是纯解释器,没有初始翻译功能:每次执行程序时都会重新读取(然后忽略)注释。

  • (EXAMPLE 1.11 Library routines and linking)典型的Fortran实现更接近于纯编译型语言。编译器将Fortran源代码翻译成机器语言。然而,它通常依赖于不属于原始程序一部分的子程序库,例如数学函数(sin、cos、log等)和I/O。编译器依赖一个称为链接器的程序,它负责将适当的库例程合并到最终程序中:

    在某种程度上,库例程可以当作硬件指令集的扩展,同时认为编译器为虚拟机生成代码,虚拟机中包括硬件和库。

    Fortran的库例程中包含关于格式化输出的解释。Fortran允许使用format语句来控制对齐、浮点数的有效位数和表示方法、前导零的控制等。程序也可以动态计算自己的格式。输出库例程包括一个格式化解释器。类似的解释器可以在C的printf中找到。

  • (EXAMPLE 1.12 Post-compilation assembly)许多编译器生成汇编语言而不是机器语言,这种方式有助于调试,因为汇编语言更易于理解,并且还可以将编译器与操作系统隔离开。当新版本的操作系统强制更新了机器语言格式时,可以不用改变编译器(只有汇编器需要更改,并且编译器之间可以共享汇编器):

  • (EXAMPLE 1.13 The C Preprocessor)C编译器(也包括其它运行在Unix系统上的编译器)会首先使用预处理器来移除注释和扩展宏。预处理器也可以插入和删除代码,还可以通过条件编译来从一份源代码编译出不同版本的程序:

  • (EXAMPLE 1.14 Source-to-source translation)很多编译器的输出依然是高级语言(通常是C语言或输入语言的简化版本)。这种高级语言到高级语言的转换经常出现在实验室语言和语言发展的早期阶段。一个著名的例子是AT&T的C++原始编译器,尽管它生成的是C而不是汇编程序,但这确实是一个真正的编译器:它对C++源程序进行了完整的语法和语义分析,并且报告了大多数的编译错误信息。事实上程序员通常不知道编译器的背后是否存在C编译器。除非生成没有错误的C代码并且再次进行编译,否则C++编译器不会调用C编译器:

    有时人们会称C++编译器为预处理器,这是因为它输出了高级语言,然后再进行编译。我认为这种叫法是不对的:因为编译器试图“理解”源代码,而预处理器没有。预处理器基于简单的模式匹配执行转换,当运行到后续的转换阶段时,这些转换的输出将生成错误消息。

  • (EXAMPLE 1.15 Bootstrapping)许多编译器都是自托管的:它们都是由其所能编译的语言编写的——Ada语言的Ada编译器,C语言的C编译器。这就产生了一个明显的问题:第一个编译器时怎么编译的?答案是使用一种称为自举的技术,这一术语源于一种荒谬的想法,即通过拉动自己的靴子将自己从地面上抬起来。简而言之,我们从一个简单的实现(通常是解释器)开始,并使用它逐步构建更复杂的版本。我们可以用一个真实的例子来说明这个想法。

    许多早期的Pascal编译器都是围绕Niklaus Wirth发布的一组工具构建的,这组工具包括以下内容:

    • 一种用Pascal编写的Pascal编译器,它生成P-code,这是一种基于堆栈的语言,类似于现代Java编译器的字节码

    • P-code形式的上述编译器

    • 用Pascal编写的P-code解释器

      为了在本地运行Pascal,用户只需将P-code解释器(手动)翻译成某种可用的语言,这项翻译工作并不困难,因为解释器很小。使用P-code解释器运行P-code版本的编译器,可以实现将任意Pascal程序编译成P-code,然后在解释器上运行P-code。为了获得更快的实现,用户可以修改Pascal版本的编译器来生成本地可用的汇编语言或机器语言,而不是生成P-code(这是一项比较困难的任务)。此时编译器就可以自举了——通过自身编译自身——实现生成机器码版本的编译器:

      推广一下,假设我们正在为一种新的编程语言构建首个编译器。如果我们的目标系统上有一个C编译器,我们可以使用C语言的子集编写一个新语言的子集编译器,这两个子集的功能是等价的。一旦这个编译器可以使用了,我们就可以手动将之前的C语言翻译成新语言,然后通过该编译器进行编译。之后,我们可以扩展编译器以支持新语言的更多功能,然后再次自举,一直这样重复下去,来支持更多的功能。这种类型的“自托管”实现非常常见。

DESIGN & IMPLEMENTATION 1.3 The early success of Pascal

The P-code-based implementation of Pascal, and its use of bootstrapping, are largely responsible for the language’s remarkable success in academic circles in the 1970s. No single hardware platform or operating system of that era dominated the computer landscape the way the x86, Linux, and Windows do today8. Wirth’s toolkit made it possible to get an implementation of Pascal up and running on almost any platform in a week or so. It was one of the first great successes in system portability.

  • (EXAMPLE 1.16 Compiling interpreted languages)某些语言(如Lisp、Prolog、Smalltalk)的编译器允许延迟绑定,这些语言通常是解释型语言。一般情况下,这些编译器必须能够生成具备解释功能的大部分代码,或者生成调用解释功能的库代码。在特殊情况下,编译器可以对一些直到运行时才确定的决策做出合理的假设,并以此生成代码。如果这些假设成立,那么代码将运行得非常快;如果假设不成立,那么动态检查将发现不一致,这时会返回解释器。

  • (EXAMPLE 1.17 Dynamic and just-in-time compilation)在某些情况下,编程系统可能会将编译延迟到最后一刻。例如语言实现(比如Lisp或Prolog)可以动态调用编译器,将新创建的源代码翻译成机器语言,或者对特定输入集的代码进行优化。另一个例子是Java的实现。Java语言定义了一种独立于机器的中间形式,称为Java字节码。字节码是Java程序分发的标准格式,它使得程序可以方便的在互联网上进行传输,然后在任何平台上运行,最早的Java实现基于字节码解释器。现代实现则通过在每次执行程序之前将字节码翻译成机器语言获得了更好的性能:

    C#也同样采用了实时翻译技术。C#编译器生成CIL(Common Intermediate Language),然后在执行之前将其翻译成机器语言。CIL独立于语言,因此各种编译器前端都可以生成CIL。我们将在16.1节中详细探讨Java和C的实现。

  • (EXAMPLE Microcode (firmware))一些机器(特别是那些在20世纪80年代中期之前设计的机器)的汇编指令实际上并没有在硬件中实现,而是在解释器上运行。解释器以称为微码(或固件)的低级指令编写,微码存储在只读存储器中,由硬件执行。5.4.1节进一步讨论了微码和微程序设计。

前面这些示例表明,编译器并不一定会将高级语言转换为机器语言。事实上,对于有些编译器的输入,我们可能根本不会将其视为程序。例如,像TEX这样的文本格式化程序会将文档描述编译成激光打印机或照排机的命令;(许多激光打印机本身包含Postscript页面描述语言的解释器。)数据库系统会将SQL等查询语言转换为对文件的基本操作;甚至有的编译器会将逻辑电路转换为计算机芯片的照片掩模。虽然本书的重点是命令式编程语言,但是我们将从一种非平凡语言翻译到另一种语言,并充分分析输入的含义的过程都称为“编译”。

8. 在本书中,我们将使用术语“x86”来指代Intel 8086及其后代的指令集体系结构,包括Pentium、Core和Xeon处理器。Intel称这种架构为IA-32,但x86是一个更通用的术语,它也包含了AMD等竞争对手的产品。

results matching ""

    No results matching ""