为什么每次程序 运行 时解释器都要编译代码?

Why Do Interpretors Compile the Code Everytime a Program is Run?

我的问题是关于所有解释性语言,但为了更好地说明我的观点,我将使用 Java 作为示例。

我对 Java 的了解是,当程序员编写代码时,他们必须将其编译为 java 字节码,这就像通用 java 虚拟机架构的机器语言.然后他们可以将他们的代码分发到任何 运行 是 Java 虚拟机 (JVM) 的机器。然后,JVM 只是一个程序,每次我 运行 我的程序时,它都会获取 java 字节代码并编译它们(针对特定体系结构)。根据我的理解(如果我在这里错了请纠正我)如果我 运行 我的代码 JVM 会即时编译它,我的机器会 运行 编译指令,当我关闭程序时编译工作会丢失,只能重新做,第二次我要运行我的程序。这也是一般解释型语言运行缓慢的原因,因为它们每次都必须即时编译。

然而,这一切对我来说毫无意义。为什么不在我的机器上下载 java 字节代码,让 JVM 为我的特定架构编译一次并创建一个可执行文件,然后下次我想要 运行 程序时,我只是 运行编译后的可执行文件。通过这种方式,java 的承诺:"write once, run everywhere" 仍然保持不变,但没有解释语言的大部分缓慢。

我知道在编译 JVM 时会进行一些巧妙的动态优化;然而,它们的目的不只是为了弥补解释机制的缓慢吗?我的意思是,如果 JVM 必须编译一次,运行 多次,那么这不会超过 JVM 完成的优化的加速吗?

我想我在这里明显遗漏了一些东西。有人有解释吗?

这是不正确的:

The JVM is then just a program that takes the java byte codes and compiles them (for the specific architecture) every time I run my program.

JVM 包含字节码解释器优化字节码编译器。它解释程序的字节码,并且仅在出于性能原因需要时将字节码编译为本机代码,以优化代码中的 "hot spots"。

要回答为什么不存储编译结果的问题,有几个问题:

  • 它需要一个地方来存放它。
  • 它需要处理已编译部分与不值得编译(或编译整个部分)的部分的拼接。
  • 它需要一种可靠的方式来了解缓存的编译代码是否是最新的。
  • 如果程序 运行 带有参数 X、Y 和 Z,则带有参数 A、B 和 C 的程序 运行 的优化编译代码可能不是最优的。所以它将不得不处理这种可能性,要么事后猜测原始编译或记住参数(这将是不完美的:例如,如果它们是文件名或 URL,它不会保留它们的内容)等。
  • 这在很大程度上是不必要的,编译不会花那么长时间。将字节码编译成机器码的时间几乎没有将源代码编译成机器码的时间长。

所以我认为答案是:困难且容易出错,因此得不偿失。

关于 "interpreters" 所做的任何此类陈述都受到观察,即并非所有口译员都是相同的。

例如,Python 解释器获取 .py 源文件并 运行s 它们。在途中,它会生成 "compiled" .pyc 文件。下次您 运行 相同的 .py 文件时,如果 .py 文件未更改,则可以跳过 "compilation" 步骤。 (我在引号中说 "compilation" 因为 AFAIK 结果不是机器代码)。

现在,进入Java。当然,Java 系统可以设计为 Java 编译器输出机器代码模块(或等效的汇编代码文件),然后可以将其链接到特定于机器的可执行映像中。但是设计师不想那样做。他们专门打算编译成虚拟机的指令集,后者解释字节码。

随着时间的推移,JVM 已经开始通过将字节码部分翻译成机器码来优化它们。但这与翻译整个程序不是一回事。

关于 compile/interpret 权衡:一个因素是您的程序执行多长时间以及它在更改之前需要多长时间。如果您 运行ning 短 "student" 程序可能在更改之前只执行一次,那么在编译上投入大量精力就没有意义了。另一方面,如果您的程序正在控制一个可能会开机数周的设备,那么在设备中进行 JIT 编译是值得的,如果设备重新启动,再次进行编译也不是特别麻烦。

我们中有些人编写 Java 代码,在一种特定的硬件配置上 运行 可能更喜欢 "compile the whole thing and be done with it",但这不是这种特定语言采用的方法。我想原则上有人可以编写该编译器,但它的不存在似乎证实没有这样做的动机。

Why Do Interpretors Compile the Code Everytime a Program is Run?

他们没有。解释器 从不 编译。它解释。如果它编译了,它将是一个编译器,而不是解释器。

解释器解释,编译器编译。

My question is about all interpreted languages, but to illustrate my point better I will use Java as an example.

没有解释语言这样的东西。使用解释器还是编译器纯粹是实现的特性,与语言完全无关。

每种语言都可以由解释器或编译器实现。绝大多数语言对每种类型至少有一个实现。 (例如,有用于 C 和 C++ 的解释器,还有用于 JavaScript、PHP、Perl、Python 和 Ruby 的编译器。)此外,大多数现代语言实现实际上结合了解释器和编译器(甚至多个编译器)。

语言只是一组抽象的数学规则。解释器是一种语言的几种具体实现策略之一。这两个生活在完全不同的抽象层次上。如果英语是一种打字语言,术语 "interpreted language" 将是打字错误。陈述 "Python is an interpreted language" 不仅仅是错误的(因为错误意味着该陈述甚至是有道理的,即使它是错误的),它只是简单地没有 sense,因为一种语言 永远不会 被定义为 "interpreted."

What I know for Java is that when programmers write their code they have to compile it in java byte codes which are like machine language for a universal java virtual machine architecture. Then they can distribute their code to any machine which runs the Java Virtual Machine (JVM).

事实并非如此。 Java 语言规范中没有任何内容需要字节码。那里甚至没有任何东西需要编译 Java 。解释 Java 或将其编译为本机机器代码是完全合法且符合规范的,事实上,两者都已完成。

此外,我很好奇:在这一段中,您将 Java 描述为一种始终编译的语言,而在上一段中,您将 Java 用作解释型语言的示例.这毫无意义。

The JVM is then just a program that takes the java byte codes and compiles them (for the specific architecture) every time I run my program.

同样,Java 虚拟机规范中没有任何关于编译或解释的内容,更不用说编译代码的时间或频率了。

解释JVML字节码是完全合法合规的,编译一次也是完全合规的,事实上,两者都做到了。

From my understanding (please correct me if I am wrong here) if I run my code the JVM will compile it on the fly, my machine will run the compiled instructions, and when I close the program all the compilation work will be lost, only to be done again, the second time I want to run my program.

这完全取决于您使用的 JVM,您使用的 JVM 的版本,有时甚至取决于特定的环境 and/or 命令行参数。

一些 JVM 解释字节码(例如 Sun 的 JVM 的旧版本)。一些版本编译字节码一次(例如Excelsior.JET)。一些版本在开始时解释字节码,在程序 运行 时收集分析信息和统计信息,使用这些数据找到所谓的 "hot spots" (即最常执行的代码,因此从加速中获益最多的代码),然后使用分析数据编译这些热点以进行优化(例如 IBM J9、Oracle HotSpot)。有些人使用类似的技巧,但有一个非优化的快速编译器而不是解释器。一些缓存并重新使用已编译的本机机器代码(例如现在废弃的 JRockit)。

This is also the reason why generally interpreted languages are slow, because they have to compile every time on the fly.

谈论一种语言是慢还是快是没有意义的。语言不慢也不快。语言就是一张纸

一段特定的代码 运行 在特定环境下的特定环境中的特定语言的特定执行引擎的特定版本上的特定代码段在特定环境下的特定硬件上可能会或可能不会变慢比另一段特定代码 运行 在另一特定环境下在另一特定环境下在另一特定环境下在另一特定硬件上的另一特定语言的另一特定版本上,但这与语言无关.

总的来说,性能主要是钱的问题,少部分是执行环境的问题。 运行 在 MacPro 上 Windows 上使用 Microsoft Visual C++ 编译的一段用 C++ 编写的特定代码确实可能比 运行 在 [=132= 上编写的一段类似代码更快] 由 YARV 在 MacPro 上 Windows 执行。

然而,其主要原因是微软是一家巨型公司,已将大量资金、研究、工程、人力和其他资源投入到 Visual C++ 中,而 YARV 主要是志愿者的努力。此外,大多数主流操作系统,如 Windows、macOS、Linux、各种 BSD 和 Unice 等,以及大多数主流 CPU 架构,如 AMD64、x86、PowerPC、ARM、SPARC, MIPS、Super-H 等针对加速类 C 语言的程序进行了优化,而对类 Smalltalk 语言的优化要少得多。事实上,一些功能甚至会伤害它们(例如,虚拟内存会显着增加垃圾收集延迟,即使它在内存管理语言中完全没用)。

However, all of this makes no sense for me. Why not download the java byte codes on my machine, have the JVM compile them for my specific architecture once and create an executable file, and then the next time I want to run the program, I just run the compiled executable file.

如果那是你想要的,没有人能阻止你。例如,这正是 Excelsior.JET 所做的。没有人强迫您使用 IBM J9 或 Oracle HotSpot。

I know that while compiling the JVM does some clever dynamic optimizations; however isn't their purpose only to compensate for the slowness of the interpretation mechanism?

那些动态优化可能恰好因为它们是动态的。编程中有几个根本不可能的结果严重限制了静态提前编译器可以进行的优化类型。停机问题、赖斯定理、函数问题等

例如,在像 Java 这样的语言中内联需要 Class 层次结构分析。换句话说,编译器需要证明一个方法没有被覆盖,以便能够内联它。事实证明,Class 动态加载语言中的层次结构分析等同于解决停机问题。因此,静态编译器只能在有限的情况下内联,在一般情况下它可以不会判断一个方法是否被覆盖。

在运行时编译代码的动态 JIT 编译器不需要证明方法未被覆盖。它不需要在运行时静态计算 Class 层次结构。 class 层次结构就在那里:它可以简单地 :方法是否被覆盖?因此,动态编译器可以在比静态编译器更多的情况下内联。

但还有更多。动态编译器还可以执行反优化。现在,您可能想知道:为什么要 de 优化?为什么让代码更糟?好吧,原因如下:如果你知道你可以反优化,那么你可以根据猜测进行优化,当你猜错时,你可以再次取消优化.

与我们的内联示例保持一致:与静态编译器不同,我们的动态编译器可以 100% 准确地确定方法是否被覆盖。但是,它不一定知道被覆盖的方法是否会被 调用 。如果覆盖的方法永远不会被调用,那么内联 superclass 方法仍然是安全合法的!所以,我们聪明的动态编译器可以做的是内联 superclass 方法 anyway 但在开始时进行一点类型检查,以确保接收者对象是否曾经是子对象class 类型,我们取消优化回到未内联的版本。这称为 推测内联,静态 AOT 编译器 根本 无法做到。

多态内联缓存 是现代高性能语言执行引擎(如 HotSpot、Rubinius 或 V8)执行的更复杂的优化。

I mean, if the JVM has to compile once, run multiple time, then wouldn't this outweigh the speed-up of the optimizations done by the JVM?

那些动态优化基本上对于静态优化器来说是不可能的。