编程语言简介/解释型程序
解释器以不同的方式执行程序。它们不生成本机二进制代码,至少通常情况下不生成。相反,解释器将程序转换为中间表示,通常是树,并使用算法遍历这棵树来模拟每个节点的语义。在上一章中,我们用 Prolog 实现了一个小型解释器,用于一种编程语言,该语言的程序表示算术表达式。即使那是一个非常简单的解释器,它也包含了解释过程的所有步骤:我们有一个表示编程语言抽象语法的树,以及一个 访问者 遍历这棵树,执行一些与解释相关的任务。
源程序在解释器中以其原始格式毫无意义,例如,一系列 ASCII 字符。因此,与编译器类似,解释器必须解析源程序。但是,与编译器不同,解释器不需要在执行程序之前解析所有源代码。也就是说,只有程序执行流可以访问的程序文本部分需要被翻译。因此,解释器做了一种延迟翻译。
解释器相对于编译器的主要优势是可移植性。如前所述,编译器生成的二进制代码是专门针对目标计算机体系结构定制的。另一方面,解释器直接处理源代码。随着 万维网 的兴起,以及从远程服务器下载和执行程序的可能性,可移植性成为一个非常重要的问题。因为 客户端 Web 应用程序必须在许多不同的机器上运行,因此浏览器下载远程软件的二进制表示并不有效。相反,必须提供源代码。
编译后的程序通常比解释后的程序运行速度快,因为编译后的程序和底层硬件之间存在更少的中间环节。但是,我们必须牢记,编译程序是一个漫长的过程,正如我们之前所见。因此,如果程序只打算执行一次,或者最多执行几次,那么解释它可能比编译并运行它更快。这种类型的场景在客户端 Web 应用程序中很常见。例如,JavaScript 程序通常被解释,而不是编译。这些程序从远程 Web 服务器下载,并且一旦浏览器会话过期,它们的代码通常就会丢失。
更改程序的源代码是应用程序开发过程中的常见任务。使用编译器时,每次更改都意味着潜在的长时间等待。编译器需要翻译修改后的文件,并链接所有二进制文件以创建一个可执行程序,然后再运行该程序。程序越大,这种延迟就越长。另一方面,由于解释器不会在运行之前翻译所有源代码,因此测试修改所需的时间明显更短。因此,解释器倾向于有利于软件 原型 的开发。
示例:bash 脚本: bash 脚本 是一个典型的解释器,常用于 Linux 操作系统。该解释器为用户提供一个 命令行界面,也就是说,它为用户提供一个 提示符,用户可以在其中输入命令。这些命令被读取并解释。命令也可以组合在一个文件中。一个bash 脚本是一个文件,其中包含要由 bash shell 执行的命令列表。Bash 是一种 脚本 语言。换句话说,bash 使用户能够非常容易地调用用 bash 本身之外的其他编程语言实现的应用程序。这样的脚本可以用来自动执行用户经常需要执行的一系列命令。以下几行是可以通过脚本文件存储的非常简单的命令,例如名为 my_info.sh
的文件
#! /bin/bash
# script to present some information
clear
echo 'System date:'
date
echo 'Current directory:'
pwd
脚本中的第一行(#! /bin/bash
)指定了应该使用哪个 shell 来解释脚本中的命令。通常,操作系统会提供多个 shell。在本例中,我们使用 bash。第二行(# script to present some information
)是 注释,在执行脚本时没有任何效果。bash 脚本的生命周期比 C 程序的生命周期简单得多。可以使用文本编辑器(如 vim)编辑脚本文件。之后,需要更改其在 Linux 文件系统中的 权限,以便使其可执行。可以通过在文件名之前加上其在 文件系统 中的位置来执行脚本调用。因此,用户可以通过在 shell 中键入 "path/my_info.sh" 来运行脚本,其中 path 表示找到脚本所需的路径
$> ./my_info.sh System date: Seg Jun 18 10:18:46 BRT 2012 Current directory: /home/IPL/shell
虚拟机 是在软件中模拟的硬件。它将解释器、运行时支持系统和解释代码可以使用的库集合组合在一起。通常,虚拟机解释类似汇编的程序表示。因此,虚拟机弥合了编译器和解释器之间的差距。编译器转换程序,将其从高级语言转换为低级 字节码。然后,这些字节码被虚拟机解释。
虚拟机的最重要的目标之一是可移植性。虚拟化的程序直接由虚拟机执行,这样程序开发者就可以忽略虚拟机运行的硬件。例如,Java 程序是虚拟化的。事实上,Java 虚拟机 (JVM) 可能是当今使用最广泛的虚拟机。任何支持 Java 虚拟机的硬件都可以运行 Java 程序。在这种情况下,虚拟机确保所有不同的程序都具有相同的语义。描述 Java 程序这一特性的口号是 "一次编写,随处运行"。这个口号说明了 Java 的 跨平台 优势。为了保证这种一致的行为,每个 JVM 都附带一个非常大的软件库,即 Java 应用程序编程接口。该库的部分内容由编译器以特殊方式处理,并在虚拟机级别直接实现。例如,Java [线程] 就是以这种方式处理的。
Java 编程语言如今非常流行。Java 运行环境的可移植性是其流行的关键因素之一。Java 最初被构想为嵌入式设备的编程语言。然而,在 Java 发布时,万维网也正迎来革命性的首次亮相。在 90 年代初期,对能够下载并在 网络浏览器 中执行的程序的需求量很大。Java 凭借 Java 小程序 填补了这一空白。如今,与 JavaScript 和 Flash 程序等其他替代方案相比,Java 小程序已经不再受宠。然而,当其他技术开始在 Web 应用程序的客户端流行起来时,Java 已经是世界上使用最广泛的编程语言之一。而且,在最初的 Web 革命多年之后,世界见证了计算机历史的新篇章:智能手机 作为通用硬件的崛起。可移植性再次成为首要因素,而 Java 再次成为这一新兴市场的重要角色。 Android 虚拟机 Dalvik 旨在运行 Java 程序。
即时编译
[edit | edit source]通常,编译后的程序运行速度比解释后的程序快。但是,在某些情况下,解释代码运行速度更快。例如,shootout 基准测试游戏包含一些比等效 C 程序更快的 Java 基准测试。这种效率背后的核心技术是 即时编译器,简称 JIT。JIT 编译器在解释程序时将其转换为二进制代码。这种设置为 推测性 代码优化提供了许多可能性。换句话说,JIT 编译器可以访问程序正在操作的运行时值;因此,它可以使用这些值来生成更好的代码。JIT 编译器的另一个优点是,它不需要编译程序的每个部分,而只需要编译执行流可以访问的那些部分。即使在这种情况下,解释器也可能决定仅编译函数中执行频率最高的那些部分,而不是整个函数体。
下面的程序提供了一个玩具 JIT 编译器的示例。如果执行正确,程序将打印 Result = 1234
。根据操作系统采用的保护机制,程序可能无法正确执行。特别是,应用了 数据执行保护 (DEP) 的系统将不会运行此程序直到结束。我们的“JIT 编译器”将一些汇编指令转储到一个名为 program
的数组中,然后将执行转移到此数组。
#include <stdio.h>
#include <stdlib.h>
int main(void) {
char* program;
int (*fnptr)(void);
int a;
program = malloc(1000);
program[0] = 0xB8;
program[1] = 0x34;
program[2] = 0x12;
program[3] = 0;
program[4] = 0;
program[5] = 0xC3;
fnptr = (int (*)(void)) program;
a = fnptr();
printf("Result = %X\n",a);
}
通常,JIT 的工作方式类似于上面的程序。它编译解释后的代码,并将编译结果(二进制代码)转储到一个标记为可执行的内存数组中。然后,JIT 更改解释器的执行流,使其指向新写入的内存区域。为了让读者对 JIT 编译器有一个大致的了解,下图展示了 Trace Monkey,它是 Mozilla Firefox 浏览器用来运行 JavaScript 程序的编译器之一。
TraceMonkey 是一个 基于跟踪 的 JIT 编译器。它不编译整个函数。相反,它只将函数中最常执行的路径转换为二进制代码。TraceMonkey 建立在名为 SpiderMonkey 的 JavaScript 解释器之上。SpiderMonkey 解释字节码。换句话说,JavaScript 源文件被转换为一系列类似汇编的指令,这些指令由 SpiderMonkey 解释。解释器还会监控执行频率较高的程序路径。当某个程序路径达到一定的执行阈值后,它将被转换为机器代码。这种机器代码被称为跟踪,即一系列线性指令。然后,跟踪通过 nanojit(Tamarin JavaScript 引擎中使用的 JIT 编译器)转换为本地代码。当该跟踪的执行结束时(无论是正常结束还是异常情况),控制权将返回到解释器,解释器可能会找到其他需要编译的跟踪。