【问题标题】:Why Do Interpretors Compile the Code Everytime a Program is Run?为什么解释器每次运行程序时都要编译代码?
【发布时间】:2019-08-07 12:08:51
【问题描述】:

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

我对 Java 的了解是,当程序员编写他们的代码时,他们必须将其编译成 Java 字节码,这类似于通用 Java 虚拟机架构的机器语言。然后他们可以将他们的代码分发到任何运行 Java 虚拟机 (JVM) 的机器上。 JVM 只是一个程序,每次我运行我的程序时,它都会获取 java 字节码并编译它们(针对特定架构)。根据我的理解(如果我在这里错了,请纠正我)如果我运行我的代码,JVM 将即时编译它,我的机器将运行编译后的指令,当我关闭程序时,所有的编译工作都会丢失,只有再次完成,第二次我想运行我的程序。这也是一般解释语言速度慢的原因,因为它们每次都必须即时编译。

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

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

我想我在这里缺少一些明显的东西。有人解释一下吗?

【问题讨论】:

    标签: java compilation jvm jit interpreted-language


    【解决方案1】:

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

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

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

    我的问题是关于所有解释型语言的,但为了更好地说明我的观点,我将使用 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 上运行用 Microsoft Visual C++ 在 Windows 上编译的 C++ 编写的特定代码确实可能比在 MacPro 上运行由 YARV 在 Windows 上执行的用 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 编译器不需要证明方法未被覆盖。它不需要在运行时静态计算类层次结构。类层次结构就在那里:它可以简单地:方法是否被覆盖?因此,动态编译器在比静态编译器更多的情况下可以内联。

    但还有更多。动态编译器也可以执行去优化。现在,您可能想知道:为什么要de-优化?为什么让代码变得更糟?好吧,原因如下:如果你知道你可以反优化,那么你可以根据 猜测 进行优化,如果你猜错了,那么你可以再次删除优化。 p>

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

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

    我的意思是,如果 JVM 必须编译一次,运行多次,那么这不会超过 JVM 所做的优化的加速吗?

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

    【讨论】:

      【解决方案2】:

      关于“口译员”做什么的任何此类陈述都取决于并非所有口译员都相同的观察。

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

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

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

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

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

      【讨论】:

        【解决方案3】:

        这是不正确的:

        JVM 只是一个程序,每次我运行我的程序时,它都会获取 java 字节码并编译它们(针对特定架构)。

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

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

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

        所以我认为答案是:困难且容易出错,因此成本不值得收益。

        【讨论】:

        • 优化后的代码不仅针对特定的程序参数定制,而且针对当前环境,即使在单次运行期间也可能(或者更确切地说将会)改变。因此JVM会在环境变化时对代码进行反优化,以创建适应新条件的新优化代码。而这些条件很少能与程序下次启动的环境相匹配。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2021-02-25
        • 1970-01-01
        • 2016-10-20
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多