我的问题是关于所有解释型语言的,但为了更好地说明我的观点,我将以Java为例。
我对Java的了解是,当程序员编写代码时,他们必须用java字节码编译它,这就像通用Java虚拟机架构的机器语言。然后,他们可以将代码分发到运行 Java 虚拟机 (JVM) 的任何机器上。然后,JVM只是一个程序,每次我运行程序时,它都会获取java字节码并编译它们(针对特定的体系结构)。根据我的理解(如果我在这里错了,请纠正我)如果我运行我的代码,JVM将即时编译它,我的机器将运行编译的指令,当我关闭程序时,所有的编译工作都将丢失,只能再次完成,第二次我想运行我的程序。这也是为什么一般解释语言很慢的原因,因为它们每次都必须动态编译。
然而,这一切对我来说毫无意义。为什么不在我的机器上下载java字节码,让JVM针对我的特定架构编译一次并创建一个可执行文件,然后下次我想运行该程序时,我只需运行编译的可执行文件。通过这种方式,Java的承诺:"一次编写,到处运行"仍然保持不变,但没有解释语言的大部分缓慢。
我知道编译JVM时会进行一些聪明的动态优化;但是它们的目的不就是为了补偿解释机制的缓慢吗?我的意思是,如果 JVM 必须编译一次,运行多次,那么这不会超过JVM完成优化的速度吗?
我想我在这里缺少一些明显的东西。有人有解释吗?
这是不正确的:
然后,JVM只是一个程序,每次我运行程序时,它都会获取java字节码并编译它们(针对特定的体系结构)。
JVM包含一个字节码解释器和一个优化字节码编译器。它解释程序的字节码,并且仅在出于性能原因必要时将字节码编译为本机代码,以优化代码中的"热点"。
要回答您为什么不存储该编译结果的问题,有几个问题:
它- 需要一个地方来存储它。
- 它需要处理编译部分与不值得编译的部分的拼接(或编译整个内容)。
- 它需要一种可靠的方法来了解缓存的编译代码是否是最新的。
- 如果使用参数 X、Y 和 Z 运行程序,则使用参数 A、B 和 C 运行的程序的优化编译代码可能不是最佳的。因此,它必须处理这种可能性,要么对原始编译进行二次猜测,要么记住参数(这将是不完美的:例如,如果它们是文件名或URL,它不会保留它们的内容)等。
- 这在很大程度上是不必要的,编译不会花那么长时间。将字节码编译为机器代码并不像将源代码编译为机器代码所花费的时间。
所以我认为答案是:这很困难且容易出错,所以成本不值得。
任何关于"口译员"所作所为的陈述都受到观察,即并非所有口译员都是一样的。
例如,Python 解释器获取.py源文件并运行它们。 在途中,它会生成"编译"的.pyc文件。下次运行相同的.py文件时,如果.py文件尚未更改,则可以跳过"编译"步骤。 (我在引号中说"编译",因为 AFAIK 结果不是机器代码)。
现在,进入Java。当然,Java 系统可以设计成 Java 编译器输出机器代码模块(或等效的汇编代码文件),然后可以链接到特定于机器的可执行映像中。但设计师不想那样做。他们专门打算编译成虚拟机的指令集,后者解释字节码。
随着时间的推移,JVM已经开始通过将字节码转换为机器码来优化字节码的各个部分。但这与翻译整个程序不是一回事。
关于编译/解释权衡:一个因素是程序执行多长时间以及更改之前需要多长时间。如果您正在运行简短的"学生"程序,这些程序在更改之前可能只执行一次,那么在编译上投入大量精力就没有意义了。 另一方面,如果您的程序正在控制可能开机数周的设备,则设备中的 JIT 编译是值得的,如果设备重新启动,再次执行此操作并不是特别麻烦。
我们中的一些编写在一种特定硬件配置上运行的Java代码的人可能更喜欢"编译整个事情并完成它",但这不是这种特定语言采用的方法。 我想原则上有人可以编写该编译器,但它的不存在似乎证实了没有动力这样做。
为什么解释器每次运行程序时都会编译代码?
他们没有。解释器从不编译。它解释。如果它编译,它将是一个编译器,而不是解释器。
解释器解释,编译器编译。
我的问题是关于所有解释型语言的,但为了更好地说明我的观点,我将以Java为例。
没有解释性语言这样的东西。使用解释器还是编译器纯粹是实现的特征,与语言完全无关。
每种语言都可以由解释器或编译器实现。绝大多数语言至少具有每种类型的一个实现。(例如,有C和C++的解释器,也有JavaScript,PHP,Perl,Python和Ruby的编译器。此外,大多数现代语言实现实际上结合了解释器和编译器(甚至多个编译器)。
语言只是一组抽象的数学规则。口译员是语言的几种具体实施策略之一。这两者生活在完全不同的抽象级别上。如果英语是一种打字语言,则术语"解释性语言"将是类型错误。"Python 是一种解释型语言"这句话不仅是错误的(因为如果说错,也意味着这个陈述是有意义的),它只是没有意义,因为一种语言永远不能被定义为"解释的"。
我对Java的了解是,当程序员编写代码时,他们必须用java字节码编译它,这就像通用Java虚拟机架构的机器语言。然后,他们可以将代码分发到运行 Java 虚拟机 (JVM) 的任何机器上。
事实并非如此。Java 语言规范中没有任何内容需要字节码。里面甚至没有任何需要编译Java的东西。解释Java或将其编译为本机机器代码是完全合法且符合规范的,事实上,两者都已经完成。
另外,我很好奇:在这一段中,您将 Java 描述为一种始终被编译的语言,但在上一段中,您使用 Java 作为解释语言的示例。这是没有道理的。
然后,JVM只是一个程序,每次我运行程序时,它都会获取java字节码并编译它们(针对特定的体系结构)。
同样,Java 虚拟机规范中根本没有关于编译或解释的内容,更不用说编译代码的时间或频率了。
解释 JVML 字节码是完全合法且符合规范的,编译一次也是完全合规的,事实上,两者都已经完成。
根据我的理解(如果我在这里错了,请纠正我)如果我运行我的代码,JVM将即时编译它,我的机器将运行编译的指令,当我关闭程序时,所有的编译工作都将丢失,只能再次完成,第二次我想运行我的程序。
这完全取决于您使用的JVM,您使用的JVM的哪个版本,有时甚至取决于特定的环境和/或命令行参数。
一些JVM解释字节码(例如Sun的JVM的旧版本)。有些版本编译字节码一次(例如Excelsior.JET)。一些版本在开始时解释字节码,在程序运行时收集分析信息和统计信息,使用此数据查找所谓的"热点"(即最常执行的代码,因此从加速中受益最大的代码),然后使用分析数据编译这些热点以进行优化(例如 IBM J9, 甲骨文热点)。有些使用类似的技巧,但有一个非优化的快速编译器而不是解释器。一些缓存并重用编译的本机机器代码(例如现在废弃的JRockit)。
这也是为什么一般解释语言很慢的原因,因为它们每次都必须动态编译。
谈论一种语言是慢的还是快的没有意义的。语言不慢不快。语言只是一张纸。
在特定环境中,在特定环境中,在特定硬件上的特定特定执行引擎的特定版本上运行的特定代码段可能比在另一个特定环境中的另一个特定特定语言的另一个特定执行引擎上运行的另一个特定版本上运行的另一个特定代码段慢,也可能不慢,也可能不慢 在另一个特定环境中另一个特定硬件下的另一个特定硬件上运行的另一个特定代码段的情况,但这与语言无关。
一般来说,业绩主要是金钱问题,在较小程度上是执行环境的问题。在MacPro上运行一段用C++Microsoft Visual C++在Windows上编译的特定代码确实可能比在MacPro上运行由Windows上的YARV执行的用Ruby编写的类似代码段更快。
然而,主要原因是Microsoft是一家大公司,在视觉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 编译器不需要证明方法未被重写。它不需要静态计算运行时类层次结构的内容。类层次结构就在那里:它可以简单地查看:该方法是否被覆盖?因此,动态编译器可以在比静态编译器更多的情况下内联。
但还有更多。动态编译器还可以执行取消优化。现在,您可能想知道:为什么要取消优化?为什么要让代码变得更糟?好吧,原因如下:如果你知道你可以取消优化,那么你可以根据猜测进行优化,当事实证明你猜错了时,你可以再次删除优化。
与我们的内联示例保持一致:与静态编译器不同,我们的动态编译器可以 100% 准确地确定方法是否被覆盖。但是,它不一定知道是否将调用重写的方法。如果重写的方法从未被调用,那么内联超类方法仍然是安全合法的!因此,我们聪明的动态编译器可以做的是内联超类方法,但在开头放一点类型检查,以确保如果接收器对象是子类类型,我们将取消优化回到非内联版本。这称为推测内联,是静态 AOT 编译器根本无法做到的。
多态内联缓存是现代高性能语言执行引擎(如 HotSpot、Rubinius 或 V8)执行的更复杂的优化。
我的意思是,如果 JVM 必须编译一次,运行多次,那么这不会超过JVM完成优化的速度吗?
对于静态优化器来说,这些动态优化从根本上是不可能的。