关于编译器结构:编译和解释之间有什么区别?

What is the difference between compilation and interpretation?

我刚和一个同事谈过,在哪里谈论的是V8 JavaScript引擎。根据维基百科,

V8 compiles JavaScript to native machine code [...] before executing it, instead of more traditional techniques such as interpreting bytecode or compiling the whole program to machine code and executing it from a filesystem.

在哪里(如果我错了请纠正我)"解释字节码"是Java的工作方式,"编译整个程序"将适用于C语言或C++语言。现在我们在想,讨论和提出错误的断言和关于不同点,相似点的假设。为此,我建议向专家咨询。

那么,谁能够

  • 命名、解释和/或引用所有主要方法(例如预编译与运行时解释)
  • 设想或提供一个关于来源、汇编和解释之间关系的方案
  • 举例说明1的主要方法(命名编程语言)。
  • 笔记:

    • 我不是在找一篇关于不同范例的冗长乏味的文章,而是一篇视觉支持的快速概述。
    • 我知道StackOverflow并不是为程序员编写的百科全书(而是为更具体的问题提供一个问答平台)。但是,由于我能找到很多流行的问题,这类问题为某些主题(如[1]、[2]、[3]、[4]、[5])提供了一个百科全书式的观点,所以我开始了这个问题。
    • 如果此问题更适合任何其他StackExchange站点(例如,CStheory),请让我知道或标记此问题以进行适度处理。


    因为一个简单的原因,回答你的问题几乎是不可能的:没有几种方法,它们是相当连续的。这个连续体中涉及的实际代码也相当相同,唯一的区别是事情发生时,以及中间步骤是否以某种方式保存。这个连续体上的不同点(不是一条直线、一个级数,而是一个有不同角的矩形,你可以靠近它)是:

  • 正在读取源代码
  • 理解代码
  • 执行你所理解的
  • 在路上缓存各种中间数据,或者将它们持久保存到磁盘上。
  • 例如,一种纯解释的编程语言几乎不会在1和3之间隐式地发生,所以您几乎不会注意到它。它只读取代码的各个部分,并立即对它们作出反应。这意味着实际开始执行的开销较低,但例如,在循环中,相同的文本行将被读取并重新读取。

    Diagram of the balance of an Interpreter (not much caching going on)

    在矩形的另一个角中,传统上有编译语言,通常情况下,项4包含永久性地将实际机器代码保存到一个文件中,然后可以在以后运行该文件。这意味着您在开始时要等待相当长的时间,直到整个程序被转换(即使您只调用其中的一个函数),但是Oth循环更快,因为不需要再次读取源代码。

    Diagram of the balance of a Compiler (mostly caching)

    还有一些介于两者之间的东西,例如虚拟机:为了可移植性,许多编程语言不是编译成实际的机器代码,而是编译成一个字节代码。然后有一个编译器生成字节码,还有一个解释器获取这个字节码并实际运行它(实际上是"将其转换为机器码")。虽然这通常比编译和直接进入机器代码要慢,但是将这种语言移植到另一个平台更容易,因为您只需要移植字节码解释器,而字节码解释器通常是用高级语言编写的,这意味着您可以使用现有的编译器来完成"有效的机器代码翻译",而不必o为要运行的每个平台创建并维护后端。此外,如果您可以执行一次对字节码的编译,然后只分发已编译的字节码,这样其他人就不必花费CPU周期(例如,在代码上运行优化器),并且只需支付字节码到本机转换的费用(在您的使用案例中可以忽略不计)。另外,您没有分发源代码。

    另一个介于两者之间的是实时编译器(just-in-time compiler,JIT),它是一个有效的解释器,以编译的形式保存它运行过一次的代码。这种"保留"使其比纯解释器慢(例如,增加了开销和RAM使用,导致交换和磁盘访问),但在重复执行一段代码时使其更快。它也可以比纯粹的代码编译器更快,例如只重复调用一个函数,因为如果不使用它,它不会浪费时间编译程序的其余部分。

    最后,您可以在这个矩形上找到其他点,例如,不永久保存编译的代码,而是再次从缓存中清除编译的代码。通过这种方式,您可以节省嵌入式系统上的磁盘空间或RAM,但可能需要再次编译一段很少使用的代码。许多JIT编译器都会这样做。


    现在许多执行环境使用字节码(或类似的代码)作为代码的中间表示。因此,源代码首先被编译成一种中间语言,然后由虚拟机解释(对字节码指令集进行解码),或者进一步编译成机器代码,然后由硬件执行。

    很少有生产语言可以在没有预编译成某种中间形式的情况下进行解释。然而,这样的解释器很容易概念化:只需为每种语言元素(if语句、for等)考虑一个具有子类的类层次结构,并且每个类都有一个Evaluate方法来评估给定的节点。这也称为解释器设计模式。

    例如,考虑以下代码片段在假设的解释器中实现if语句(在C中实现):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    class IfStatement : AstNode {
        private readonly AstNode condition, truePart, falsePart;

        public IfStatement(AstNode condition, AstNode truePart, AstNode falsePart) {
            this.condition = condition;
            this.truePart = truePart;
            this.falsePart = falsePart;
        }

        public override Value Evaluate(EvaluationContext context) {
            bool yes = condition.Evaluate(context).IsTrue();
            if (yes)
                truePart.Evaluate(context);
            else
                falsePart.Evaluate(context);
            return Value.None; // `if` statements have no value.
        }
    }

    这是一个非常简单但功能齐全的解释器。