【发布时间】:2009-12-10 04:50:33
【问题描述】:
我听说 Java 必须使用 JIT 才能快。与解释相比,这非常有意义,但是为什么有人不能制作一个能够生成快速 Java 代码的提前编译器呢?我知道 gcj,但我认为它的输出通常不会比 Hotspot 快。
语言中是否存在使这变得困难的因素?我认为归结为以下几点:
- 反射
- 类加载
我错过了什么?如果我避免这些功能,是否可以将 Java 代码编译为原生机器代码并完成?
【问题讨论】:
我听说 Java 必须使用 JIT 才能快。与解释相比,这非常有意义,但是为什么有人不能制作一个能够生成快速 Java 代码的提前编译器呢?我知道 gcj,但我认为它的输出通常不会比 Hotspot 快。
语言中是否存在使这变得困难的因素?我认为归结为以下几点:
我错过了什么?如果我避免这些功能,是否可以将 Java 代码编译为原生机器代码并完成?
【问题讨论】:
JIT 编译器可以更快,因为机器代码是在它也将在其上执行的确切机器上生成的。这意味着 JIT 拥有可用的最佳信息来发出优化的代码。
如果您将字节码预编译为机器码,编译器无法针对目标机器进行优化,只能针对构建机器进行优化。
【讨论】:
我会将James Gosling给出的有趣答案粘贴到书Masterminds of Programming中。
嗯,我听说过 实际上你有两个编译器 Java 世界。你有编译器 到 Java 字节码,然后你有 你的 JIT,它基本上会重新编译 一切都特别清楚。所有的 你可怕的优化在 JIT.
詹姆斯:没错。这些天我们 击败真正优秀的 C 和 C++ 编译器几乎总是。当你 去动态编译器,你得到 编译器的两个优点 在最后一刻运行。一 你知道到底是什么芯片组吗 你正在运行。很多时候当 人们正在编译一段 C 代码,他们必须编译它才能运行 在通用 x86 上 建筑学。几乎没有 你得到的二进制文件特别好 为其中任何一个进行了调整。你下载 Mozilla 的最新版本,它会 几乎可以在任何英特尔上运行 架构 CPU。有很多 一个 Linux 二进制文件。很一般, 它是用 GCC 编译的,即 不是一个很好的 C 编译器。
当 HotSpot 运行时,它确切地知道 你正在运行什么芯片组。它 确切地知道缓存是如何工作的。它 确切地知道内存层次结构 作品。它确切地知道所有的 流水线互锁在 CPU 中工作。 它知道什么指令集 该芯片具有的扩展。它 精确优化什么机器 轮到你了。然后是另一半 是它实际上看到了 应用程序在运行时。它能够 有统计数据知道哪些 事情很重要。它能够 C 编译器可以内联的东西 永远不会做。得到的东西 Java 世界中的内联很漂亮 惊人的。然后你把它附加到 存储管理的工作方式 现代垃圾收集器。带一个 现代垃圾收集器,存储 分配速度极快。
【讨论】:
任何 AOT 编译器的真正杀手是:
Class.forName(...)
这意味着您不能编写涵盖 所有 Java 程序的 AOT 编译器,因为只有在运行时才能获得有关程序特征的信息。但是,您可以在 Java 的一个子集上执行此操作,我相信 gcj 就是这样做的。
另一个典型的例子是 JIT 能够在调用方法中直接内联 getX() 等方法,如果发现这样做是安全的,并在适当的情况下撤消它,即使程序员没有明确帮助通过告诉一个方法是最终的。 JIT 可以看到在运行的程序中给定的方法没有被覆盖,因此在这种情况下可以被视为最终方法。这在下一次调用中可能会有所不同。
2019 年编辑:Oracle 引入了 GraalVM,它允许在 Java 的一个子集(一个相当大的,但仍然是一个子集)上进行 AOT 编译,主要要求是所有代码在编译时都可用。这允许 Web 容器的毫秒启动时间。
【讨论】:
Java 的 JIT 编译器也是惰性和自适应的。
因为懒惰,它只编译方法而不是编译整个程序(如果你不使用程序的一部分,这非常有用)。类加载实际上有助于让 JIT 更快,因为它允许它忽略尚未遇到的类。
作为自适应,它首先发出一个快速而肮脏的机器代码版本,然后只有在频繁使用该方法时才返回并完成工作。
【讨论】:
归根结底,拥有更多信息可以实现更好的优化。在这种情况下,JIT 有更多关于代码正在运行的实际机器的信息(正如 Andrew 所提到的),它还有很多在编译期间不可用的运行时信息。
【讨论】:
Java 跨虚拟方法边界内联和执行高效接口调度的能力需要在编译之前进行运行时分析 - 换句话说,它需要 JIT。由于所有方法都是虚拟的,并且接口“无处不在”,因此差别很大。
【讨论】:
理论上,JIT 编译器优于 AOT如果它有足够的时间和可用的计算资源。例如,如果您的企业应用程序在具有大量 RAM 的多处理器服务器上运行数天或数月,则 JIT 编译器可以生成比任何 AOT 编译器更好的代码。
现在,如果您有一个桌面应用程序,那么快速启动和初始响应时间(AOT 的优势所在)之类的东西就变得更加重要,而且计算机可能没有足够的资源来进行最高级的优化。
如果你有一个资源稀缺的嵌入式系统,JIT 就没有机会对抗 AOT。
然而,以上都是理论。实际上,创建这样一个高级 JIT 编译器比一个像样的 AOT 编译器要复杂得多。一些practical evidence怎么样?
【讨论】:
JIT 可以识别和消除一些只能在运行时知道的条件。一个典型的例子是消除现代 VM 使用的虚拟调用 - 例如,当 JVM 找到 invokevirtual 或 invokeinterface 指令时,如果仅加载了一个覆盖所调用方法的类,则 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();
假设我们不在其他地方创建A 或B 实例并且someCondition 设置为true,JVM 知道对doIt() 的调用总是意味着A.doIt,并且可以因此避免方法表查找,然后内联调用。在非 JITted 环境中的类似构造不会是可内联的。
【讨论】:
我认为官方 Java 编译器是 JIT 编译器这一事实是其中很大一部分。 JVM 与 Java 机器码编译器的优化花费了多少时间?
【讨论】:
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 远不能满足它所宣传的所有内容以及人们盲目吐槽的理论。
【讨论】: