编译和解释的区别是什么?

18
I just had a conversation with a colleague and we were discussing the V8 JavaScript engine. According to Wikipedia, V8 compiles JavaScript to native machine code before executing it, instead of interpreting bytecode or compiling the whole program to machine code and executing it from a filesystem, which are more traditional techniques. Interpreting bytecode is the way Java works, while compiling the whole program would apply for languages like C or C++. We were debating and making false assertions about the differences and similarities between these methods. To get a clear answer, I suggested asking experts on SO. Can someone on SO please: 1) name, explain, and/or reference all major methods (e.g. precompiling vs. runtime interpretation); 2) visualize or provide a scheme about the relations between source, compilation, and interpretation; and 3) give examples (name programming languages) for the major methods of #1?
注释:
  • 我不需要一篇冗长的关于不同范式的散文,而是需要一个可视化支持的、快速概述。
  • 我知道Stackoverflow并不旨在成为程序员的百科全书(而是更专注于针对特定问题的问答平台)。但是,由于我可以找到很多流行的问题,这些问题提供了某些主题的百科全书式视角(例如[1][2][3][4][5]),所以我发起了这个问题。
  • 如果这个问题更适合其他StackExchange网站(例如cstheory),请告诉我或者标记这个问题进行审核。

4
很惊讶这个问题的投票数不多,因为它是一个重要问题,并且有一些很棒的答案。 - Raydot
2个回答

18

由于一个简单的原因,回答你的问题几乎不可能:没有几种方法,而是更像一个连续体。在这个连续体中涉及到的实际代码也相当相似,唯一的区别就是事情发生的时间以及是否以某种方式保存了中间步骤。 这个连续体上的各个点(它不是一条线、一个进展,而是更像是一个矩形,有不同的角落可以靠近)包括:

  1. 阅读源代码
  2. 理解代码
  3. 执行理解的内容
  4. 在路上缓存各种中间数据,或者甚至将它们永久地保存到磁盘上。

例如,一个完全的解释型编程语言基本上不会进行#4操作,在1和3之间的#2隐含地发生,所以你几乎不会注意它。它只是读取代码的某些部分,并立即对其做出反应。这意味着实际开始执行的开销很小,但是例如在循环中,同样的文本行会被反复读取。

Interpreter平衡图(没有太多缓存)

在矩形的另一个角落,传统上有一些编译型语言,其中通常#4项包括将实际机器代码永久保存到文件中,然后以后可以运行。这意味着你要等比较长的时间,在开始时整个程序都被翻译(即使你只调用其中的一个函数),但是循环速度更快,因为不需要再次读取源代码。

Compiler平衡图(大多数缓存)

然后,还有一些中间状态的东西,例如 虚拟机:为了便携性,许多编程语言不会编译成实际的机器代码,而是编译成字节码。然后有一个编译器生成字节码,一个解释器接受这个字节码并实际运行它(有效地“将其转换成机器代码”)。虽然这通常比直接编译并转移到机器代码要慢,但是将这样的语言移植到另一个平台更容易,因为您只需要移植字节码解释器,这通常是用高级语言编写的,这意味着您可以使用现有的编译器来做到这一点“有效的翻译成机器代码”,而不必为您想要运行的每个平台制作和维护后端。此外,如果您可以将编译到字节码的编译只执行一次,然后只分发编译好的字节码,则可能会更快,这样其他人就不必在例如运行优化程序的CPU周期上花费时间,而只需支付从字节码到本机代码的转换,这在您的使用情况下可能是微不足道的。此外,您不会公开源代码。

另一个中间状态的东西是 即时编译器(JIT),它实际上是一个保留已经运行过的代码的解释器,以编译形式保存。这种“保留”使其比纯解释器更慢(例如增加了开销和内存使用量,导致交换和磁盘访问),但在反复执行一段代码时会更快。如果只重复调用单个函数的代码,则它也可能比纯编译器更快,因为它不会浪费时间编译未使用的程序部分。

最后,您可以找到此矩形上的其他位置,例如不永久保存已编译的代码,而是再次从缓存中清除已编译的代码。这样,您可以节省嵌入式系统上的磁盘空间或RAM,代价可能是必须第二次编译很少使用的代码。许多JIT编译器都这样做。


@konrad-rudolph 请看下面的解释。对于翻译整个程序并运行每个表达式一次,它会更慢(绝对上),但由于在实践中几乎从不发生这种情况,因为至少某些部分通常会执行两次或根本不执行,JIT更快。但是,如果要举一个JIT比纯解释器慢的例子,请看“Hello World”示例。 - uliwitness
1
我不太信服。同样地,编译后的代码比解释执行的代码要慢,但这种说法显然是误导性的,除了一些琐碎的情况(例如你的“Hello World”示例),它根本就不正确。在这两种情况下,“比纯解释器慢”都不是一个有信息量的陈述。 - Konrad Rudolph
1
@konrad-rudolph 我在这里解释了不同方法和权衡的理论,这是选择任何混合方法(例如JIT编译器/解析器/高速缓存构造)的权重所必需的。 这正是您需要在此处了解的非实用性理论情况。 然后,您才会查看您的具体实际用例(对于网站弹出窗口、Web输入字段过滤器、一次性批处理操作或系统编程有所不同),并决定哪种组合最适合您。 - uliwitness
1
@konrad-rudolph 我在“比纯解释器慢”的后面添加了一个限定词,以便举例说明哪一部分实际上更慢。此外,可以查看Mac上的Safari,以获取几乎所有方法的示例。主机本身是编译的,这是最快的执行方式。JavaScript通常用于像onLoad这样的一次性操作(开销最小),字节码用于运行几次的内容(开销更大但不需要重复解析),而像输入字段过滤器这样反复运行的内容则使用新的FTL功能进行编译。 - uliwitness
@ImranRafiqRather,“slower”是一个模糊的词。正如括号中的评论所提到的,代码的“启动时间”会变慢(“开销”)。我并不是说当代码开始运行各个指令时,它会运行得更慢。如果您想了解更多细节,请阅读这篇文章的详细版本:http://orangejuiceliberationfront.com/the-difference-between-compiler-and-interpreter/ - uliwitness
显示剩余3条评论

3
现在许多执行环境使用字节码(或类似物)作为代码的中间表示。因此,源代码首先被编译成一种中间语言,然后由虚拟机(解码字节码指令集)解释,或者进一步编译成机器代码并由硬件执行。
目前很少有生产语言是未经预编译为某种中间形式而解释的。然而,可以轻松地构想这样一个解释器:只需将每种语言元素类型(if语句、for等)的子类包含在类层次结构中,并且每个类都具有Evaluate方法来评估给定节点。这也通常称为解释器设计模式
例如,考虑以下代码片段,它实现了一个假想解释器中的if语句(用C#实现)。
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.
    }
}

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


“真正的解释器”是一个不幸的选择。这是关于层次和视角的问题。可以争论甚至字节码解释器也不是“真正的解释器”,因为它需要有人将源代码编译成字节码,然后才能解释它。一个“真正的解释器”逐行或逐语句读取一小段源代码,并在识别出每个语句时立即执行它。字节码解释器也是一种解释器,但只有在忽略前面的编译步骤(将其转换为VM或虚拟CPU)时才是如此。 - uliwitness
@uliwitness 我不同意这种评估。这里只是有两个不应混淆的不同阶段:Python解释器一个真正的解释器,尽管它解释的是字节码而不是Python代码 - 要么逐条语句(这被称为REPL),要么一次处理一个文件。原始源代码被编译成中间格式,但这并不影响中间格式被解释的事实,而且这一步的实现被归类为解释器。 - Konrad Rudolph
但是你在哪里停止这种区分呢?如今,英特尔CPU包含一个解释器,将英特尔指令集翻译成实际的类RISC硬件指令。从那个角度来看,所有的代码都是解释的。我们需要在这里区分术语。传统上,“解释性编程语言”意味着源代码是解释的,而不是字节码。有一个名为“解释器”的算法(字节码解释器使用),以及OP所问的“解释性编程语言”的更高级概念。我们不应混淆它们。 - uliwitness
Konrad:按照这种推理(经典的预JIT),Java也是一个解释器,因为它编译成字节码,而虚拟机是一个解释器。将整个过程命名为其中一个阶段是危险的。编译的一个良好工作定义是将代码翻译到更接近机器的级别。这也适用于字节码生成。 - Marco van de Voort
@Marco 但是Java的JIT并不解释字节码。它将其编译为机器码。 - Konrad Rudolph
显示剩余6条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接