跳转到内容

使用 C 和 C++ 的编程语言概念 / 语言处理器

来自 Wikibooks,开放世界开放书籍

在本章中,我们将研究不同类型的语言处理器,同时将使用称为墓碑图的图形表示法。然而,不要因为大量使用这种符号而误以为图形表示本身具有任何意义。除非对它们所代表的底层概念有清晰的理解,否则随意添加图表来创建一些花哨的图形毫无价值。

在我们对主题的论述中,将介绍基本概念、程序和机器,然后回顾语言处理技术、翻译和解释。在此基础上,我们将探索更复杂的语言处理器使用方式,以构建更高效的环境。

基本图表

[编辑 | 编辑源代码]

从计算机科学家的角度来看,一个程序是一组规则模式,用于指导计算过程的演变。[1]它由特定符号系统中的符号表达式组成,该符号系统称为编程语言

A program named P written using language L
使用语言 L 编写的名为 P 的程序

一个机器是一个自动机 [当给定一个程序时],可以执行任务来简化其受益者的生活。从计算机科学家的角度来看,术语机器是计算机或任何等效数学模型的同义词。

A machine named M
名为 M 的机器

运行程序

[编辑 | 编辑源代码]

程序只有以适当的机器代码形式表达才能在 [物理] 机器上运行。[2]

Running a program named P on machine named M
在名为 M 的机器上运行名为 P 的程序


翻译器

[编辑 | 编辑源代码]

一个翻译器是一个程序,它接受以一种语言(翻译器的源语言)表达的任何文本,并生成以另一种语言(其目标语言)表达的语义等效文本。

特别是,一个编译器从高级语言翻译到低级语言(不仅仅是机器代码);一个汇编器从汇编语言翻译到相应的机器代码;一个反编译器将低级语言程序翻译成高级语言程序;一个反汇编器将机器代码程序翻译成汇编语言程序。

S-into-T translator expressed in language L
用语言 L 表达的 S 到 T 的翻译器

墓碑的头部用箭头分隔,命名了翻译器的源语言 S 和目标语言 T。墓碑的底部命名了翻译器的实现语言 L。

翻译程序

[编辑 | 编辑源代码]
Translating a source program P expressed in language S to an object program expressed in language T, using an S-into-T translator
使用 S 到 T 的翻译器,将以语言 S 表达的源程序 P 翻译成以语言 T 表达的目标程序

源程序和目标程序在语义上是等效的。[3]对源程序和目标程序使用相同的名称是一种广泛使用的约定,以强调这一事实。

交叉编译器

[编辑 | 编辑源代码]

一个交叉编译器是一个在某台机器(主机)上运行,但为另一台不同机器(目标机)生成代码的编译器。目标程序是在主机上生成的,但必须传输到目标机器上才能运行。这种传输通常称为下载

Translating and running a program
翻译和运行程序

两阶段翻译器

[编辑 | 编辑源代码]

两阶段翻译器是两个翻译器的组合。当您想将新的语言实现移植到不同的平台时,这种方案很有用。您只需将新的语言翻译成一种广泛使用的编程语言,然后将该翻译的输出翻译成机器代码。现在,您就在所有存在该无所不在的语言编译器的平台上拥有了编程语言实现。

Two-stage translation
两阶段翻译

更正式地说,我们可以说语义等效关系是等价关系。也就是说,它是自反的、对称的和传递的。因此,我们可以轻松地将这个想法推广到多个阶段:一个 n 阶段翻译器是 n 个翻译器的组合,涉及 n-1 个中间语言。

翻译翻译器

[编辑 | 编辑源代码]

翻译器,无论是编译器、汇编器还是任何类型的翻译器,都只是另一段软件。因此,与其他程序一样,它本身也可以作为输入被输入到翻译器中。(翻译器唯一特别的地方,如果可以称之为特别,就是它们将其他程序作为输入。)

Translating a translator
翻译翻译器

从下面的输入和输出图中可以看出,翻译器只是普通的程序

Translators as plain programs
翻译器作为普通的程序

解释器

[编辑 | 编辑源代码]

在翻译中,必须将整个源程序翻译成其目标程序等效程序,才能开始运行并产生结果。这可以比作翻译一部小说:它一次性地全部翻译完。

另一方面,一个解释器是一个程序,它接受以特定语言(源语言)表达的任何程序(源程序),并立即运行该源程序。这种方法更像同声传译员的工作方式:她在说话者发表声明时进行翻译;她不会等到他说完话才开始翻译。

类似于微程序的取指-译码-执行循环,解释器通过逐条取指、分析和执行源程序指令来工作。每次解释一条源代码指令时,它首先被取指,然后进行分析,包括将其翻译成物理机器的指令,最后通过执行相应的机器代码指令来执行。

当以下情况成立时,解释是有意义的

  • 指令具有简单的格式,可以轻松有效地进行分析。(请注意指令和指令格式之间的区别。)
  • 指令很复杂;它们的执行需要很长时间,以至于用于取指和分析的时间可以忽略不计。
  • 每条指令只执行一次(或者至少不经常执行)。
  • 程序员在交互模式下工作,希望在输入下一条指令之前看到每条指令的结果。
  • 该程序仅使用一次后即被丢弃,因此运行速度并不重要。

对前两项的推理可以通过分析在物理机器上执行源代码指令的成本来理解。

(执行源代码指令的成本)

运行源代码指令的总成本等于所有阶段成本的总和。执行阶段成本(大致)等于执行相应机器语言指令的成本之和。请注意,每个源代码指令对应多条机器语言指令,这是很自然的,因为源语言是更高层次的语言。

当源代码指令格式简单时, 太小,将被 掩盖。也就是说,我们将有

总成本几乎等于执行相应机器代码指令的成本。因此,由于解释而损失的不多。

在复杂的源代码指令的情况下, 太大,将支配指令的运行时间。因此,我们最终得到先前的结论:

尽可能少地执行源代码指令意味着由于获取和分析阶段而产生的成本不会重复支付。因此,仍然有理由将解释视为一种替代方案。

第四项基本上是对解释背后的想法的重新阐述:指令一次执行一条。解释和交互性之间的这种并行关系体现在解释器的最佳示例来自命令 shell 世界,例如 bash、csh 等。

最后一个项目的典型例子是应用程序的原型设计,其中开发了一个轻量级版本的应用程序,用于通过在开发过程的早期阶段向客户展示它来确保正确获取用户需求。由于这样的应用程序只使用几次,并且不需要很快,因此解释被证明是一个不错的选择。

相反,当以下情况发生时,解释不是一个明智的选择:

  • 该程序将在生产模式下运行,因此速度很重要。
  • 指令被频繁执行。例如,包含许多 for 循环的算法不适合解释。
  • 指令格式复杂,因此分析起来很耗时。
问题
在实现矩阵操作模块时,我们应该选择哪种类型的语言?
问题
在实现一个自动执行多个程序协调执行的脚本时,我们应该选择哪种类型的语言?
问题
在兼容硬件上运行机器代码程序涉及哪种类型的处理?

解释程序

[edit | edit source]
在 M 中实现的 S-解释器的墓碑图
通过在 S-解释器上解释程序 P 来运行程序 P

请注意,整个程序没有进行翻译;指令被一次一条地获取、分析和执行,就像你在为 S 的抽象机器运行你的程序一样。有关更多信息,请参见有关抽象机器的部分。

真实机器和抽象机器

[edit | edit source]

解释可用于测试新设计的硬件,而无需实际构建它。这种方法通过缩短设计-构建-调试周期为你节省了大量时间。请考虑以下典型场景。

  1. 设计人员提出硬件设计或修改以前构建的设计。
  2. 该设计既可以以软件形式构建,也可以以硬件形式构建。但是,印刷电路板(即以硬件形式构建设计)不是您在办公室就能做的事情;您需要找一些芯片制造公司来为您做。而且,如果您没有大批量订单,您一定会等很长时间。因此,硬件选项比较耗时,会损害您的竞争力。另一方面,软件选项使您能够通过在现有计算机上运行新设计机器的程序来测试您的设计,并且可以在您的办公室完成。
  3. 在测试新硬件的过程中,可能会检测到设计错误。如果是这样,您需要回到第一步。否则,您可以进行下一步。
  4. 市场营销人员开始进行销售推介,将新产品推向市场,因为您已印刷了电路板。

这种解释——即在另一台机器上运行尚未构建的机器的程序——被称为仿真。仿真器不能用于测量被仿真机器的绝对速度。但它仍然可以用来获取有用的定量信息,例如内存周期数、并行度等。

我们可以用高级编程语言(如 C)编写解释器。

Emulator written in a high-level programming language
用高级编程语言编写的仿真器

该程序被进一步编译成用机器代码 M 表达的另一个解释器。

Translating the emulator
翻译仿真器

我们现在可以为新硬件编写程序。

Program emulation
程序仿真

在解释器之上运行程序在功能上等同于直接在机器上运行同一个程序。用户在程序的输入和输出方面看到相同行为。这两个过程甚至在细节上也很相似:解释器以取指令-分析-执行循环工作,而机器以取指令-译码-执行循环工作。唯一的区别是解释器是软件制品,而机器是硬件制品(因此速度快得多)。

因此,机器可以被看作是硬件实现的解释器。反之,解释器可以被看作是软件实现的机器。因此,解释器有时被称为抽象机器,以区别于其硬件对应物——称为[实际]机器。所以,我们可以定义机器代码为一种存在硬件解释器(至少在纸面上)的语言。

如果抽象机器和实际机器都实现了相同的语言 L,那么它们在功能上是等效的。

Equivalence of an abstract machines and interpreters
抽象机器和解释器的等效性

解释型编译器

[edit | edit source]

编译器可能需要很长时间才能将源程序翻译成机器代码,但目标程序将以全速运行。解释器允许程序立即开始运行,但它会运行得很慢。

解释型编译器是编译器和解释器的组合,它兼具两者的一些优点。关键思想是将源程序翻译成中间语言,然后在运行中间语言程序的抽象机器上解释翻译结果。

中间语言设计为满足以下要求

  • 它的级别介于源语言和普通机器代码之间。
  • 它的指令格式简单,因此可以轻松快速地分析。
  • 从源语言到中间语言的翻译简单快捷。

使用中间语言作为目标语言的两个缺点是运行速度和反编译的容易程度。前者意味着资源利用率低下,而后者意味着缺乏对产品保护。为了解决第二个缺点,可以使用名为混淆器的软件,它会重命名变量并重新排列代码,使其难以理解。在实时编译器部分将详细介绍如何提高运行速度。

使用解释型编译器最大的优点是目标代码的可移植性,这意味着您只需编译一次程序即可在任何地方运行它。当您需要为使用不同架构的客户提供服务时,这种方案非常有用。事实上,下面提供的两个示例都具有这一点。

Pascal/P-code 解释型编译器

[edit | edit source]

我们的第一个例子是作为编译器套件的一部分使用,这使得 Pascal 在 70 年代后期和 80 年代初期成为学术界首选的编程语言。

Pascal/P-code 解释型编译器包含两个组件

Components of the Pascal/P-code interpretive compiler
Pascal/P-code 解释型编译器的组件

如果我们将 Pascal 程序输入到翻译器中,我们将获得相应的 P-code 程序。

Translating a Pascal program into a P-code program
将 Pascal 程序翻译成 P-code 程序

接下来,我们在 P-code 解释器上运行生成的 P-code 程序。

Running P-code programs
运行 P-code 程序

现在,P-code 是一种以 Pascal 为中心的中间语言。这意味着它提供了强大的指令,这些指令直接对应于 Pascal 操作,例如数组赋值、数组索引和过程调用。因此,从 Pascal 到 P-code 的翻译简单快捷。尽管功能强大,但 P-code 指令具有像机器代码指令一样的简单格式,带有操作码和操作数字段,因此易于分析。因此,P-code 解释相对较快。

Java/字节码解释型编译器

[edit | edit source]

我们的第二个例子是 Java 编程语言,它作为互联网语言首次亮相——可移植性的终极测试实验室。

与 Pascal/P-code 类似,Java/字节码解释型编译器包含两个组件:Java 到字节码的编译器和 Java 虚拟机 (JVM)。除了充当字节码解释器外,JVM 还提供安全、垃圾回收等服务。

Java/字节码解释型编译器工具包的组件
将 Java 源代码编译成字节码
运行字节码程序

看起来像之前的例子,不是吗?嗯,除了名称之外,它完全一样。所以,Sun 并不是第一个发现可移植性的人!

考虑到Sun 的 picojava计划,等效性可以进一步扩展,如下图所示。

Running Bytecode programs faster
更快地运行字节码程序

因此,Sun 的字节码在 Sun 的 picojava 上运行速度最快。(看起来是一个不错的营销技巧,嗯?)

问题:提供表示运行 C# 程序所需过程的图表。

实时编译器

[edit | edit source]

上述方案的一个缺点是程序的运行时间。虽然它比纯解释快得多——因为解释的是程序的较低级表示——但与机器级对应物的解释相比,它很慢。通过在流程中引入实时编译可以减轻这种负面影响。每当调用子程序时,解释器——更准确地说,虚拟机中称为实时编译器的部分——会动态地将子程序的中间代码表示编译成其机器代码等效项并执行它。由于翻译过程涉及的开销,子程序的第一次调用将很昂贵。不过,后续调用将尽可能快。

事实上,实时编译器(也称为抖动器)生成的代码最终可能比原生代码编译器生成的代码更快。这得益于抖动器中代码生成动态性的优势。考虑将您的代码库迁移到更高级的机器。由于虚拟机及其抖动器(假设它们已更新以反映新机器的新功能)了解新机器的改进,因此可以利用这些改进,您所有的代码现在都运行得更快。但是,如果您有原生代码可执行文件,情况并非如此。由于可执行文件通常由针对特定机器的实现者创建,因此迁移到不同的机器将不会导致运行速度提高。[4][5]

注释

[edit | edit source]
  1. 更一般地说,任何可以控制的对象(或人)都是编程活动的“目标”,尽管在某种程度上有所不同。
  2. 稍后,我们将放宽此说法,并说可以通过解释器在抽象机器上运行程序。
  3. 请注意,我们做出了一个简化的假设,即编译器(或汇编器)会生成可执行文件。然而,这通常不是这种情况。您可能需要将编译器(或汇编器)的输出与其他目标文件和/或库链接。
  4. 这并不意味着您将完全不会有任何提高。由于外围设备更快而导致的性能改进仍然会使您的应用程序运行得更快。
  5. 如果您可以访问源代码,则可以花一些时间重新编译项目。
华夏公益教科书