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

时间:2019-03-16 16:40:10

标签: java compilation jvm jit interpreted-language

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

我对Java的了解是,当程序员编写代码时,他们必须将其编译为Java字节码,就像通用Java虚拟机体系结构的机器语言一样。然后,他们可以将其代码分发到运行Java虚拟机(JVM)的任何计算机上。 JVM只是一个程序,它在每次运行程序时都采用Java字节代码并针对特定体系结构编译它们。根据我的理解(如果我错了,请更正)如果我运行我的代码,JVM将即时对其进行编译,我的机器将运行已编译的指令,而当我关闭程序时,所有的编译工作将会丢失,仅再次执行,这是我第二次运行程序。这也是一般解释语言之所以速度慢的原因,因为它们必须随时进行编译。

但是,所有这些对我来说都毫无意义。为什么不下载Java字节码到我的机器上,让JVM为我的特定体系结构编译一次并创建一个可执行文件,然后下次我想运行该程序时,我只运行编译的可执行文件。这样,仍然保留了Java的承诺:“编写一次,随处运行”,但是没有大多数解释语言的慢度。

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

我想我这里显然缺少一些东西。有人有解释吗?

3 个答案:

答案 0 :(得分:1)

这是不正确的:

  

然后JVM只是一个程序,它在每次运行程序时都采用java字节代码并针对特定体系结构编译它们。

JVM包含一个字节码解释器 一个优化的字节码编译器。它会解释程序的字节码,并仅出于性能原因而在必要时将字节码编译为本机代码,以优化代码中的“热点”。

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

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

所以我认为答案是:这既困难又容易出错,因此付出的代价并不值得。

答案 1 :(得分:0)

任何有关“口译员”做什么的陈述都必须遵守以下观点:并非所有口译员都是相同的。

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

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

随着时间的流逝,JVM已经开始通过将字节码部分转换为机器代码来优化它们。但这与翻译整个程序不同。

关于编译/解释权衡:一个因素是程序执行多长时间以及更改前需要多长时间。如果您正在运行简短的“学生”程序,这些程序在被更改之前只能执行一次,那么在编译上投入很多精力就没有意义。另一方面,如果您的程序正在控制可能会开启数周的设备,则值得在设备中进行JIT编译,并且在重新启动设备后再次进行操作并不特别麻烦。

我们中的一些写在一种特定硬件配置上运行的Java代码的人可能更喜欢“编译整个事情并用它完成”,但这不是该特定语言所采用的方法。我想原则上可以有人编写该编译器,但是它的不存在似乎证明没有动力这样做。

答案 2 :(得分:0)

  

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

他们没有。解释器从不进行编译。它解释。如果已编译,则它将是编译器,而不是解释器。

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

  

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

没有诸如解释语言之类的东西。使用解释器还是编译器纯粹是实现的特征,与语言完全无关。

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

一种语言只是一组抽象的数学规则。解释器是一种语言的几种具体实现策略之一。这两个人生活在完全不同的抽象级别上。如果英语是一种打字语言,则术语“解释语言”将是一种打字错误。语句“ Python是一种解释语言”不仅是错误的(因为如果错误则意味着该语句甚至是有意义的,即使它是错误的),它也不会产生 sense 的含义,因为一种语言永远不会定义为“已解释”。

  

我对Java的了解是,当程序员编写代码时,他们必须将其编译为Java字节码,就像通用Java虚拟机体系结构的机器语言一样。然后,他们可以将其代码分发到运行Java虚拟机(JVM)的任何计算机上。

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

此外,我很好奇:在本段中,您将Java描述为始终编译的语言,而在上一段中,您将Java用作解释语言的示例。那没有道理。

  

然后JVM只是一个程序,它在每次运行程序时都采用java字节代码并针对特定体系结构编译它们。

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

解释JVML字节码是完全合法且符合规范的,一次编译它也很符合规范,实际上,两者都已完成。

  

根据我的理解(如果我在这里错了,请纠正我)如果运行我的代码,JVM将即时对其进行编译,我的机器将运行已编译的指令,并且当我关闭程序时,所有编译工作将迷路了,只能第二次运行我的程序。

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

某些JVM会解释字节码(例如,Sun的JVM的旧版本)。某些版本会一次编译字节码 (例如Excelsior.JET)。某些版本在开始时会解释字节码,在程序运行时收集性能分析信息和统计信息,使用这些数据来查找所谓的“热点”(即执行最频繁的代码,因此受益最大的代码) (从加快速度开始),然后使用概要分析数据进行编译以进行优化(例如,IBM J9,Oracle HotSpot)。有些使用类似的技巧,但是使用非优化的快速编译器而不是解释器。一些缓存并重新使用已编译的本机代码(例如现在废弃的JRockit)。

  

这也是一般解释语言之所以速度慢的原因,因为它们必须随时进行编译。

谈论某种语言的慢速或快速没有任何意义。语言并不慢。语言只是一张纸。

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

通常,性能主要是金钱问题,而在较小程度上是执行环境问题。在MacPro上运行Windows上用Microsoft Visual C ++编译的用C ++编写的特定代码的确确实比在MacPro上运行Windows上由YARV执行的用Ruby编写的类似代码更快。

但是,主要原因是微软是一家巨大的公司,已经向Visual C ++投入了大量的资金,研究,工程,人力和其他资源,而YARV主要是自愿的。而且,大多数主流操作系统(例如Windows,macOS,Linux,各种BSD和Unices等)以及大多数主流CPU架构(例如AMD64,x86,PowerPC,ARM,SPARC,MIPS,Super-H等)均已优化。用于加快使用类似C语言的程序的速度,而对类似Smalltalk的语言的优化则少得多。实际上,某些功能甚至主动地破坏(例如,虚拟内存可以显着增加垃圾收集延迟,即使在内存管理语言中完全没有用)。

  

但是,所有这些对我来说都毫无意义。为什么不下载Java字节码到我的机器上,让JVM为我的特定体系结构编译一次并创建一个可执行文件,然后下次我想运行该程序时,我只运行编译的可执行文件。

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

  

我知道在编译JVM时会进行一些巧妙的动态优化。但是它们的目的不只是为了弥补解释机制的缓慢性吗?

这些动态优化只是 可能精确地,因为它们是动态的。编程中有一些根本不可能的结果,这些结果严重限制了静态提前编译器可以执行的优化类型。停止问题,莱斯定理,函数问题等。

例如,以Java之类的语言进行内联时,需要进行类层次分析。换句话说,编译器需要证明一个方法没有被重写以便能够内联它。事实证明,使用动态加载的语言进行类层次分析等效于解决停止问题。因此,静态编译器只能在有限的情况下内联,在一般情况下它不能 ,告诉方法是否被覆盖。

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

但是还有更多。动态编译器还可以执行 de-optimization 。现在,您可能会想:为什么要 de 优化?为什么使代码更糟?好吧,这就是原因:如果您知道可以取消优化,那么就可以基于 guesss 进行优化,而事实证明您猜错了,那么只需再次删除优化即可。

保留我们的内联示例:与静态编译器不同,我们的动态编译器可以100%的准确性确定方法是否被重写。但是,它不一定知道覆盖的方法是否会被调用。如果没有调用重写的方法,那么内联超类方法仍然是安全合法的!因此,我们聪明的动态编译器可以执行的操作是内联超类方法无论如何,但在开始时进行一点类型检查,以确保如果接收器对象曾经是子类类型,我们可以将其去优化非内联版本。这称为投机性内联,这是静态AOT编译器根本无法做到的。

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

  

我的意思是,如果JVM必须编译一次,多次运行,那么这难道不超过JVM完成的优化速度吗?

对于静态优化器来说,根本不可能实现这些动态优化。