【问题标题】:Why is Java faster when using a JIT vs. compiling to machine code?为什么使用 JIT 与编译为机器代码相比,Java 更快?
【发布时间】:2009-12-10 04:50:33
【问题描述】:

我听说 Java 必须使用 JIT 才能快。与解释相比,这非常有意义,但是为什么有人不能制作一个能够生成快速 Java 代码的提前编译器呢?我知道 gcj,但我认为它的输出通常不会比 Hotspot 快。

语言中是否存在使这变得困难的因素?我认为归结为以下几点:

  • 反射
  • 类加载

我错过了什么?如果我避免这些功能,是否可以将 Java 代码编译为原生机器代码并完成?

【问题讨论】:

    标签: java jit


    【解决方案1】:

    JIT 编译器可以更快,因为机器代码是在它也将在其上执行的确切机器上生成的。这意味着 JIT 拥有可用的最佳信息来发出优化的代码。

    如果您将字节码预编译为机器码,编译器无法针对目标机器进行优化,只能针对构建机器进行优化。

    【讨论】:

    • 那里不错。简洁明了。
    • 那我们为什么不对 C/C++ 使用 JIT 呢?我想这就是 LLVM 的用武之地。
    • @Ibrahim - 我们在这里谈论的是指令集的变化;例如x86 指令集的众多扩展中的哪一个在执行平台上可用。
    • 提前编译器仍然可以匹配。例如,可以告诉英特尔 C++ 编译器发出一段代码的多个版本,每个版本针对稍微不同的处理器目标进行调整。它将添加代码以在启动时自动检测处理器并选择最合适的代码路径。
    • 对于大多数程序,机器特定的优化并不重要。我认为这不是 JIT 的重点。
    【解决方案2】:

    我会将James Gosling给出的有趣答案粘贴到书Masterminds of Programming中。

    嗯,我听说过 实际上你有两个编译器 Java 世界。你有编译器 到 Java 字节码,然后你有 你的 JIT,它基本上会重新编译 一切都特别清楚。所有的 你可怕的优化在 JIT.

    詹姆斯:没错。这些天我们 击败真正优秀的 C 和 C++ 编译器几乎总是。当你 去动态编译器,你得到 编译器的两个优点 在最后一刻运行。一 你知道到底是什么芯片组吗 你正在运行。很多时候当 人们正在编译一段 C 代码,他们必须编译它才能运行 在通用 x86 上 建筑学。几乎没有 你得到的二进制文件特别好 为其中任何一个进行了调整。你下载 Mozilla 的最新版本,它会 几乎可以在任何英特尔上运行 架构 CPU。有很多 一个 Linux 二进制文件。很一般, 它是用 GCC 编译的,即 不是一个很好的 C 编译器。

    当 HotSpot 运行时,它确切地知道 你正在运行什么芯片组。它 确切地知道缓存是如何工作的。它 确切地知道内存层次结构 作品。它确切地知道所有的 流水线互锁在 CPU 中工作。 它知道什么指令集 该芯片具有的扩展。它 精确优化什么机器 轮到你了。然后是另一半 是它实际上看到了 应用程序在运行时。它能够 有统计数据知道哪些 事情很重要。它能够 C 编译器可以内联的东西 永远不会做。得到的东西 Java 世界中的内联很漂亮 惊人的。然后你把它附加到 存储管理的工作方式 现代垃圾收集器。带一个 现代垃圾收集器,存储 分配速度极快。

    【讨论】:

    • 这很有趣。我如何在我运行的机器上完全编译我的程序,这样性能会很好,我不需要整个 VM 运行。而且这种情况经常发生,因为我们在大多数机器相同的环境中运行。
    • @DzungNguyen,更好的是,使用 LLVM 之类的编译器如何生成中间代码,该中间代码在安装时编译为特定运行时环境的机器代码。记住一个非常关键的事实,JIT 是用 AOT 编写的。 JIT 可以做的任何事情,AOT 也可以。
    • @DzungNguyen 您只需选择另一个为此场景优化的 JVM。
    【解决方案3】:

    任何 AOT 编译器的真正杀手是:

    Class.forName(...)
    

    这意味着您不能编写涵盖 所有 Java 程序的 AOT 编译器,因为只有在运行时才能获得有关程序特征的信息。但是,您可以在 Java 的一个子集上执行此操作,我相信 gcj 就是这样做的。

    另一个典型的例子是 JIT 能够在调用方法中直接内联 getX() 等方法,如果发现这样做是安全的,并在适当的情况下撤消它,即使程序员没有明确帮助通过告诉一个方法是最终的。 JIT 可以看到在运行的程序中给定的方法没有被覆盖,因此在这种情况下可以被视为最终方法。这在下一次调用中可能会有所不同。


    2019 年编辑:Oracle 引入了 GraalVM,它允许在 Java 的一个子集(一个相当大的,但仍然是一个子集)上进行 AOT 编译,主要要求是所有代码在编译时都可用。这允许 Web 容器的毫秒启动时间。

    【讨论】:

    • 所有类加载都发生在运行时。我不明白你的评论。
    • 我的意思是,在字节码生成时无法预测的类加载。 Class.forName 接受一个字符串并生成一个类。没有办法知道它可能是什么类。如果您不这样做,AOT 编译器可以知道您可能使用的所有类并进行一些优化,对吧?
    • 是的,这正是它崩溃的原因。
    • known 类加载器的 Class.forName() 可以通过解析为预编译类来处理。我们为 Eclipse RCP 和 Tomcat 类加载器做到了这一点,更不用说系统和应用程序了:excelsiorjet.com
    • @Dmity,非常有趣。我不知道这是可行的。
    【解决方案4】:

    Java 的 JIT 编译器也是惰性和自适应的。

    懒惰

    因为懒惰,它只编译方法而不是编译整个程序(如果你不使用程序的一部分,这非常有用)。类加载实际上有助于让 JIT 更快,因为它允许它忽略尚未遇到的类。

    自适应

    作为自适应,它首先发出一个快速而肮脏的机器代码版本,然后只有在频繁使用该方法时才返回并完成工作。

    【讨论】:

    • 其自适应性的另一个方面是它可以在解释字节码时收集有关测试/分支可能结果的统计数据,并将其输入 JIT 编译器以生成更好的代码。
    【解决方案5】:

    归根结底,拥有更多信息可以实现更好的优化。在这种情况下,JIT 有更多关于代码正在运行的实际机器的信息(正如 Andrew 所提到的),它还有很多在编译期间不可用的运行时信息。

    【讨论】:

    • LLVM 也有相同的信息,奇怪的是 Linux 也有。通过在目标机器上编译代码,您可以获得相同或相似的好处。
    【解决方案6】:

    Java 跨虚拟方法边界内联和执行高效接口调度的能力需要在编译之前进行运行时分析 - 换句话说,它需要 JIT。由于所有方法都是虚拟的,并且接口“无处不在”,因此差别很大。

    【讨论】:

    • @28 - 理论上,您可以(保守地)找出可以在给定 Java 程序中使用的完整类集,只需检查源代码或字节码。因此,您可以静态地进行这些优化。
    • 您仍在编译大部分内容。例如基于 IO 的方法调用。
    • 一些 C++ 编译器将使用分析信息。静态分析可用于内联许多虚拟方法调用。
    • 整个程序优化在 MSVC 中完成。
    • LLVM 做同样的事情,.NET 和 Linux 也是如此。如果您在目标机器上编译代码,您可以访问 JIT 拥有的相同数据。有什么不同? C++ 方法默认不是虚拟的。
    【解决方案7】:

    理论上,JIT 编译器优于 AOT如果它有足够的时间和可用的计算资源。例如,如果您的企业应用程序在具有大量 RAM 的多处理器服务器上运行数天或数月,则 JIT 编译器可以生成比任何 AOT 编译器更好的代码。

    现在,如果您有一个桌面应用程序,那么快速启动和初始响应时间(AOT 的优势所在)之类的东西就变得更加重要,而且计算机可能没有足够的资源来进行最高级的优化。

    如果你有一个资源稀缺的嵌入式系统,JIT 就没有机会对抗 AOT。

    然而,以上都是理论。实际上,创建这样一个高级 JIT 编译器比一个像样的 AOT 编译器要复杂得多。一些practical evidence怎么样?

    【讨论】:

    • 嗯,这是一个有趣的链接,但我更感兴趣的是看到与 gcj 而不是 gcc 的比较。
    • Stefan 之前的基准测试会议 (stefankrause.net/wp/?p=6) 包括 gcj 和 Apache Harmony,但它有点过时了。此外,与这些实现的比较并不完全正确,因为它们没有经过测试以符合 Java SE 规范。在完全兼容的实现中存在一些开销,其中之一与堆栈溢出处理有关(双关语:))。
    • 上下文切换呢?每次 JIT 必须返回并细化优化时,它需要切换线程或更糟的是,整个过程。 LLVM 做了很多类似的优化,因为它生成中间代码,在目标环境中转换成机器代码。
    【解决方案8】:

    JIT 可以识别和消除一些只能在运​​行时知道的条件。一个典型的例子是消除现代 VM 使用的虚拟调用 - 例如,当 JVM 找到 invokevirtualinvokeinterface 指令时,如果仅加载了一个覆盖所调用方法的类,则 VM 实际上可以使该虚拟调用静态因此能够内联它。另一方面,对于 C 程序来说,函数指针始终是函数指针,并且不能内联对它的调用(无论如何,在一般情况下)。

    这是 JVM 能够内联虚拟调用的情况:

    interface I { 
        I INSTANCE = Boolean.getBoolean("someCondition")? new A() : new B();
        void doIt(); 
    }
    class A implements I { 
        void doIt(){ ... } 
    }
    class B implements I { 
        void doIt(){ ... } 
    }
    // later...
    I.INSTANCE.doIt();
    

    假设我们不在其他地方创建AB 实例并且someCondition 设置为true,JVM 知道对doIt() 的调用总是意味着A.doIt,并且可以因此避免方法表查找,然后内联调用。在非 JITted 环境中的类似构造不会是可内联的。

    【讨论】:

    • 我不知道你在说什么。 C 不支持虚函数,但如果你指的是 C++,它确实支持内联函数、函数和虚函数,因为它是 C 的超集。内联函数是用实际函数的代码替换函数调用的代码。这消除了设置会导致小的性能损失的堆栈帧的需要。由于可以删除整个 vtable,因此虚函数的性能略有提升。大多数体面的 C++ 编译器会警告或建议内联函数,甚至会自动为您转换。
    • 大多数 C++ 程序并没有从 JIT 正在执行的内联函数中显着受益。设置和进行函数调用所花费的时间是微不足道的,尤其是对于大型函数。相比之下,JIT 优化实际上可能会导致更大的性能损失,尤其是在函数只被调用一次的情况下。不要忘记切换进程以执行这些优化的隐藏性能成本,这比函数或虚拟函数调用慢得多。
    • @ATL_DEV 这个问题是关于提前 Java 编译的,所以 C、C++ 或任何其他非 Java(或者可能是非 JVM)语言所做的都是题外话。我的回答是关于为什么 AOT Java 会错过一些优化。
    • 好的。那么我的评论是对等的:为什么 C++ 不会从 AOT 优化中受益匪浅。
    【解决方案9】:

    我认为官方 Java 编译器是 JIT 编译器这一事实是其中很大一部分。 JVM 与 Java 机器码编译器的优化花费了多少时间?

    【讨论】:

      【解决方案10】:

      Dimitry Leskov 绝对就在这里。

      以上所有只是关于什么可以使 JIT 更快的理论,实现每个场景几乎是不可能的。此外,由于我们在 x86_64 CPU 上只有少数不同的指令集,因此针对当前 CPU 上的每个指令集几乎没有什么好处。在使用本机代码构建性能关键型应用程序时,我总是遵循以 x86_64 和 SSE4.2 为目标的规则。 Java 的基本结构造成了大量的限制,JNI 可以帮助您展示它的低效率,JIT 只是通过使其整体更快来对此进行修饰。除了默认情况下每个函数都是虚拟的这一事实之外,它还在运行时使用类类型,而不是例如 C++。 C++ 在性能方面有很大的优势,因为不需要在运行时加载类对象,所有数据块都在内存中分配,并且仅在请求时才初始化。换句话说,C++ 在运行时没有类类型。 Java 类是实际的对象,而不仅仅是模板。我不会进入 GC,因为这无关紧要。 Java 字符串也较慢,因为它们使用动态字符串池,这需要运行时每次在池表中进行字符串搜索。其中许多原因是因为 Java 最初并不是为了快速而构建的,所以它的基础总是很慢。大多数本地语言(主要是 C/C++)都是专门为精简而构建的,不会浪费内存或资源。事实上,Java 的前几个版本非常缓慢且浪费内存,其中包含大量不必要的变量元数据等等。就像今天一样,JIT 能够生成比 AOT 语言更快的代码仍然是一个理论。

      想想 JIT 需要跟踪执行惰性 JIT 的所有工作,每次调用函数时递增计数器,检查调用了多少次......等等。运行 JIT 需要很多时间。在我看来,这种交易是不值得的。这只是在PC上

      曾经尝试在 Raspberry 和其他嵌入式设备上运行 Java?绝对糟糕的表现。树莓派上的 JavaFX?甚至没有功能... Java 和它的 JIT 远不能满足它所宣传的所有内容以及人们盲目吐槽的理论。

      【讨论】:

      • C++ 最初绝对不是为了刻薄和精简而构建的,它花了很长时间才到达那里。
      • 我记得一些 C++ 编译器可以选择打开运行时类型信息或 RTTI。它实际上只是在分配类时存储的元数据。每当您请求对象的类型信息时,都会有少量存储损失和少量损失。
      猜你喜欢
      • 2021-03-25
      • 1970-01-01
      • 2012-02-06
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2023-03-29
      • 2010-12-31
      相关资源
      最近更新 更多