我们一般将计算机语言系统分为以下四类:纯解释器、解释器、编译器和增量编译器。它们处理源程序和执行其结果的方式各不相同,这将影响到各自执行程序时的效率。
纯解释器直接工作于文本源文件,往往非常没有效率。解释器持续扫描源文件(通常即ASCII文本文件),将其当作字符串数据处理。为了识别其中的词素(lexeme)—诸如保留字、文字常量等语言组件,其操作很是耗时。实际上,许多解释器在处理词素,在“词法分析”方面花的时间比它们实际执行程序还多。纯解释器可能是最小的计算机语言处理程序,这是因为所有语言转换器都要进行词法分析,而对词素实时分析的执行最省气力。正因如此,纯解释器在期望语言处理程序非常紧凑的场合很受欢迎。纯解释器还流行于脚本语言以及超高级语言中。超高级语言允许我们在程序执行期间将其源代码当作字符串来操纵。
解释器在程序运行时执行源文件的替身。这一替身并非人可阅读的文本文件。正如前面所述的那样,许多解释器是对记号化的源文件进行操作,以免执行时分析词素。有些解释器读入文本源文件后,将其转换成记号化形式再执行。这样程序员既可用其喜爱的文本编辑器工作,又能享受到以记号化格式执行程序的快速。仅有的代价就是对源文件进行记号化处理时需要耽误一点时间,不过这在多数现代计算机上几乎察觉不到;还有可能无法将字符串当作程序语句执行。
编译器将文本形式的源程序转换为可执行的机器码。处理过程很复杂,在优化型编译器中尤其如此。对编译器生成的代码要说明两点。首先,编译器生成的机器指令可由底层的CPU直接执行。因此,CPU在执行程序时,不必为解析源文件而牺牲任何周期—CPU的所有资源都用来执行机器码。故而得到的程序通常比其解释性版本要快很多倍。当然,编译器转换源代码的质量参差不齐,但即便是蹩脚的编译器,也比多数解释器做得出色。
编译器将源代码转换为机器码是单向的功能。相比解释器而言,要从给定的机器码重构得到程序原来的源文件,即使可能,也是很困难的。
增量编译器是编译器和解释器的“交集”。有各式各样的增量编译器。通常,增量编译器类似于解释器,不会将源文件编译成机器码,而是将源代码转换到某种中间形式。不过它又不像解释器,其中间形式与原始源文件的联系并不紧密。这种中间形式通常是一种虚拟机器语言的机器码,“虚拟”指没有哪个真正的CPU能执行这种代码。然而,很容易为这种虚拟机编写解释器,由解释器来实际执行代码。由于虚拟机解释器往往比记号化代码要有效率得多,执行虚拟机代码远比执行解释器中的一大堆记号快。Java等语言正是采用这种编译技术,通过解释程序— Java 字节码引擎(Java byte code engine)解释性地执行Java“机器码”的,如图4-1所示。虚拟机执行的一大优势在于虚拟机代码可移植—只要是有解释器的地方,虚拟机的程序就可以执行。相比而言,真正的机器代码只能在为之编写的CPU家族上执行。一般来说,解释的虚拟机代码比解释性代码运行快2到10倍,而纯机器代码通常又比解释的虚拟机代码快2到10倍。
图4-1 Java字节码(JBC)解释器
为了改进增量编译器所编译程序的性能,许多厂商,尤其是Java系统厂商采取了一种被称为即时编译(just-in-time compilation)的技术。该概念基于这样一个事实:在运行时期,解释器所花的相当多时间其实都花在了获得并解析虚拟机代码的操作上。在程序执行时,解释过程会反复进行。即时编译技术会在首次遇到虚拟机指令时,就将虚拟机代码转换为实际机器码。这样做,解释器就会在下次遇到程序的同一语句时(例如在循环中)省掉解释过程。即时编译技术远比不上真正的编译器,但它可以将程序的性能提高2~5倍。
注意: 有趣的是,早先的编译器以及某些免费的编译器往往将源代码编译成汇编语言代码。我们得另有一个编译器,也即汇编器,才能将输出汇编成机器码。现代的高效编译器大都不这么干了,而是一步到位。请参看4.5节来了解更多信息。
在上面所述的4种计算机语言处理器中,本章将着重说明编译器机制。通过了解编译器如何生成机器码,我们就可以选择适当的高级语言语句,得到更好、更富有效率的机器码。要想改进解释器或增量编译器所写程序的性能,最好的办法是使用优化型编译器来处理应用程序。例如,GNU提供了一款Java编译器,该编译器能够产生优化了的机器码,而非需要解释执行的Java字节码,它比Java字节码运行快得多,甚至比即时编译的字节码还快。