JVM JIT能否专门为子类中未被覆盖的方法进行优化?

4

标题不能完全表达我的意思,但基本上我的意思是,在一个类Base中给定一些方法m(),如果在某个子类Derived中没有重写该方法,则当前JVM中的JIT编译器是否能够在有意义的情况下仍然"专门化"0 m(),或者继承并且不重写Base.m()的派生类是否共享相同的编译代码?

这种专门化在派生类定义了使m()变得更简单的东西时是有意义的。例如,为了讨论而言,假设m()调用另一个成员函数n(),在派生类中,n()被定义为当n()内联到m()中时,后者大大简化。

具体来说,考虑以下类中的两个非抽象方法(它们都是m()类型的方法,而抽象方法是相应的n()方法):

public class Base {

  abstract int divisor();
  abstract boolean isSomethingEnabled();

  int divide(int p) {
    return p / divisor();
  }

  Object doSomething() {
    if (isSomethingEnabled()) {
      return slowFunction();
    } else {
      return null;
  }
}

两种方法都依赖于抽象方法。假设您现在有一个像这样的Derived

public class Derived extends Base {

  final int divisor() {
    return 2;
  }

  final boolean isSomethingEnabled() {
    return false;
  }
}

现在,divide()doSomething()方法的有效行为非常简单,divide不是对任意数进行完整的除法,而只是可以通过位运算简单地将其减半。 doSomething()方法始终返回false。我假设当JIT编译divide()doSomething()时,如果Derived是唯一的子类,则一切顺利:目前存在(当前)仅有的两个抽象调用的一个可能实现,并且CHA将启动并内联唯一可能的实现,一切都很好。
然而,在其他派生类存在的更一般情况下,我不确定JVM是否只使用invokevirtual调用编译Base中方法的一个2版本,还是聪明到说:“嘿,即使Derived没有覆盖divisor(),我也应该专门为它编译一个版本,因为它会更简单”。
当然,即使没有专门的重新编译,积极的内联通常也能使其正常工作(即,在已知或甚至只是可能是Derived的类上调用divide()时,内联可能会为您提供良好的实现,但是同样,有很多情况下这种内联不会被执行。
0 我所说的专门化并没有特定意义,除了在某些受限领域中编译另一个适当的函数版本之外,就像内联是针对特定调用点的专门化形式一样,或者就像大多数函数都针对当前上下文进行了某种程度的专门化(例如,加载的类,关于nullness的假设等)。 1 特别是,当人们说“JVM能否blah blah”时,通常是指Hotspot,并且我也主要在Hotspot中,但也包括任何其他JVM是否也可以做到这一点。 2 好吧,当然,您可能会有几个函数版本,用于栈替换,不同的编译器级别,出现去优化时等等...
4个回答

4
  1. HotSpot JVM has at most one current, entrant version of compiled method. This is obvious from one-to-one relationship between Method and nmethod entities in the source code. However, there can be multiple non-entrant previous versions (e.g. nmethods compiled at lower tier and OSR stubs).
  2. This single compiled version is often optimized for the most common case basing on run-time profiling. For example, when during profiling of Base.doSomething() JIT sees that isSomethingEnabled() is always invoked on Derived instance (even if there are more subclasses), it will optimize the call for the fast case, leaving an uncommon trap for a slow one. After this optimization doSomething() will look like

        if (this.getClass() != Derived.class) {
            uncommon_trap();  // this causes deoptimization
        }
        return false;
    

  1. 为每个分支和每个调用点单独收集配置文件数据,这使得可以针对不同的接收者优化(专门化)方法的一部分,并针对不同的接收者优化另一部分。
  2. 如果在剖析过程中检测到两个不同的接收者,则JIT可以内联由类型检查保护的两个被调用者。
  3. 具有超过两个接收器的虚拟调用将使用vtable查找编译。

要查看方法概要数据,请使用JVM的调试构建中提供的 -XX:+ PrintMethodData 选项。


谢谢,我已经从PrintCompilation输出中猜到了这一点。你可以通过将m()方法复制粘贴到Derived中(如果它在Base中不是final)来部分解决它,但这违反了DRY原则。我进行了小的编辑以澄清该方法最多只有一个“当前”版本,因为“实际”的说法有些过于强烈:先前的方法版本仍然可能同时运行等等 - 因此它们在其代码可能无限期地执行的意义上是非常真实的(至少对于长时间运行的方法而言)。如果您不同意措辞,请随时恢复原样 :) - BeeOnRope
1
@BeeOnRope 你的解释很合理。谢谢。 - apangin

2
不,我的理解是JVM不会自己专门优化某个方法,而是在分析优化过程中发现divisor()经常调用某个方法时会对基类函数进行优化。
你尝试打印诊断信息看看会发生什么了吗?
引用:
与其猜测JIT正在做什么,不如通过开启Java命令行标志来窥探发生了什么:-XX:+PrintCompilation -XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining(来自Java JIT编译器内联

我还没有从-XX:+PrintCompilation看到任何证据表明这种情况发生,尽管我认为它可能被隐藏了。话虽如此,我还没有在更近期的JVM上深入研究过它(在Java 9 JVM上根本没有研究过)。 - BeeOnRope

1
根据OpenJDK Wiki的说法:
  • 方法通常被内联。这增加了编译器的优化范围。
  • 静态、私有、final和/或“特殊”的调用易于内联。
  • 如果类层次结构允许,则虚拟(和接口)调用通常会降级为“特殊”调用。在进一步的类加载破坏事物的情况下,将注册依赖项。
  • 具有不平衡类型配置文件的虚拟(和接口)调用将编译成支持历史上常见类型(或两种类型)的乐观检查。
也就是说,对于最常见的两个接收者类型,派生方法将内联到其调用者中(如果足够小,在这种情况下应该是这样),并且无法访问的分支将被修剪。
此外,如果基本方法足够小以便内联到其调用方,则它将针对该调用点的两个最常见的接收者类型进行优化。
也就是说,Hotspot JVM会为该调用点的两个最常见的接收者类型专门编写代码,如果该代码足够小以便内联。

是的,我知道这个问题,并且在最后一段中尝试进行了解释 - 内联通常可以使其“正常工作”,特别是当整个基本方法被内联时(因为那时你真的可以修剪掉所有关于n()类型方法的内容)。我认为条件内联(又称“内联缓存”)并不像真正的内联那样强大 - 其他类型的回退代码(如果存在)无法真正被修剪掉 - 它必须存在于触发去优化的存根中,或者实际上存在。这会抑制函数中的其他优化。 - BeeOnRope
另一个问题是,这基于在m()方法的调用站点内联_,而重要的优化(针对此问题)是在m()内部的n()调用站点。前者的“可内联性”不一定与后者有关系。具体来说,想象一下m()太大而无法内联或在m()的调用站点上有太多的接收器类型以内联它:在这种情况下,将n()内联到m()中仍然可能非常有用。 - BeeOnRope

-1

JVM不定义或重新定义类型。它解释行为的运行实现。编译器,即源语言处理类型。JVM是Java宇宙的低层,“金属”。类型及其实例是创建受输入影响的一系列可观察事件的指令。该系列输入和随时间变化的可观察事件构成计算机科学家所称的程序“语义”。

由JVM找出执行这些指令的方法并保留语义。有时,它会完全破坏类结构。从概念上讲,类实例存储在带标签的堆内存中。在某段时间内,直到由于某种状态更改而禁止语义,JVM可能会在寄存器中保留两个活动值,甚至不在RAM中,并忽略整个已定义类的其余部分。这是否是“专门化”方法?

不是的。没有新的定义,没有新的Java级别指令集,没有JVM中的临时类型。只是一种临时的、编译和优化的方式来满足指令。当优化不再起作用或不那么重要时,JVM甚至会恢复到解释字节码。字节码也不包含任何新类型。它是一种汇编语言,重新定义高级代码所需的内容超出了它的工资等级。

归根结底,程序中的唯一类型是源代码中规定的类型,而不是字节码或JVM。所有语义都来自源代码。


我认为你误解了我的问题。我非常清楚JLS和JVMS对JVM施加的限制,以及它在实践中进行优化的许多方式 - 但我的问题是:_是否有任何当前的JVM应用上述特定优化_。因此,要明确的是,当我在这里说“JVM能否...”时,“能否”应被解释为“当前的实现是否能够...”,而不是“规范是否允许...”。当然,仅凭标题并不清楚,但我认为我在文本中解释得很清楚。 - BeeOnRope
我没有误解你的问题。而且你所描述的不是优化。我解释的是JVM不涉及类型。你的问题不适用于JVM。不存在类共享方法的概念,只有根据语义创建当前执行的概念。我猜这并不排除不同类的实例共享某些优化的部分,但在那个层面上,这最多只是一个比喻。与其指责别人误解,至少考虑一下其他可能性。 - Lew Bloch
1
当然,我所描述的是一种优化,就像内联、类层次分析等其他数百万种优化一样。它对最终用户来说是不可见的,也不会影响语义。你的回答几乎全部都是无关紧要的,解释了我从未提及过且与问题无关的事情。"误解"是一个慈善的解释,另一个解释是你的回答没有太多意义。你似乎固定在"specializing"这个词上,并回答了我从未打算问的某个特定含义的问题。 - BeeOnRope
无论如何,我在我的介绍中试图澄清一下,不要过多地解读“专业化”,并将其视为一个简单的问题,即这种优化是否被执行(是否允许并不是真正需要争论的 - 它是允许的)。 - BeeOnRope
我认为这不是一个有意义的问题。类似的代码可能会被同样优化,这是我最好的猜测。在JVM的层面上,你不会有“类共享重构方法”的概念。 - Lew Bloch

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接