【问题标题】:How do I convince the JVM to inline an interface method?如何说服 JVM 内联接口方法?
【发布时间】:2011-09-27 08:44:36
【问题描述】:

我有一个植根于接口并使用抽象基类实现的类层次结构。它看起来像这样:

interface Shape {
  boolean checkFlag();
}

abstract class AbstractShape implements Shape {
  private boolean flag = false;

  protected AbstractShape() { /* compute flag value */ }

  public final boolean checkFlag() { return flag; }
}

interface HasSides extends Shape { 
  int numberOfSides();
}

interface HasFiniteArea extends Shape { 
  double area();
}

class Square extends AbstractShape implements HasSides, HasFiniteArea {

} 

class Circle extends AbstractShape implements HasFiniteArea { 
}

/** etc **/

当我使用 VisualVM 对正在运行的代码进行采样时,AbstractShape.checkFlag() 似乎从未内联并消耗 14% 的总程序运行时间,这对于一个如此简单的方法来说是令人讨厌的,甚至对于如此频繁调用的方法。

我已在基类上标记了最终方法,并且(当前)所有实现“Shape”接口的类都扩展了 AbstractShape。

我是否正确解释了 VisualVM 示例结果?有什么方法可以说服 JVM 内联此方法,还是我需要删除接口并只使用抽象基类? (我不希望这样做,因为层次结构包括 HasFiniteArea 和 HasSides 等接口,这意味着层次结构没有完美的树形)

编辑:明确地说,这是一种在 any 宇宙中应该 内联的方法。在 2 分钟的执行过程中,它被调用了超过 4.2 亿次 次,并且由于它没有内联并且仍然是虚拟调用,因此它占运行时的 14%。我要问的问题是是什么阻止了 JVM 内联此方法,我该如何解决?

【问题讨论】:

  • 什么的 14%? 1毫秒?这是您可能用 Java 编写的最快的方法。它只返回字段的值。为什么要内联?
  • 这可能是分析时未完成内联的问题...
  • @ThorbjørnRavnAndersen 我认为这对于分析是正确的,但我使用 VisualVM 进行采样。这也会干扰内联吗?
  • 你的程序在剩下的 86% 的时间里做什么?

标签: java optimization jvm compiler-optimization


【解决方案1】:

这里引用the Wikipedia

一个常见的误解是将类或方法声明为 final 通过允许编译器直接插入 方法内联,无论它被调用。这并不完全正确。这 编译器无法做到这一点,因为类加载在 运行时,可能与刚才的版本不同 编译。此外,运行时环境和 JIT 编译器具有 关于哪些类已经被加载,并且能够 就何时内联做出更好的决定,无论是否 方法是最终的。

另见this article

【讨论】:

  • +1: 作为对这个答案的补充:final 方法显然不能被内联,如果它们在几个不同的类中实现!从 JVM 的角度来看,你所拥有的只是一个Shape,JIT 编译器到底应该如何知道该接口后面是否有一个 Rect 或一个 Oval,它应该知道调用哪个方法/内联?
  • @Angel O'Sphere 实际上,JIT 能够解决这个问题。这是那些讨厌的去优化之一。 “由于到目前为止所有类都扩展了 AbstractShape,我们可以直接调用最终方法 X 而无需虚拟调用开销”(理论上也适用于内联)。一旦加载了实现 Shape 而不是 AbstractShape 的类,就必须再次对其进行去优化。热点 VM 至少对于仅由单个类实现的接口是这样做的(因为这是 Java 中一个常用的习惯用法,有助于提高性能)
  • 啊,我看到了你的最后一句话;D 是的,我没有提到,因为大多数人不会理解它。如果您查看我的措辞,我明确假设将存在两个实现;D
  • @AngelO'Sphere JVM 做了很多很棒的优化。正如 Voo 所说,它确实有足够的信息来知道接口的所有(加载的)实现都继承自同一个基类并共享该方法的相同最终实现。问题是JVM 实际上 是否这样做,或者我是否需要重构我的代码才能从内联中受益。
  • 当前的 HotSpot JVM 确实做到了。我认为这里的每个人都清楚地回答了这个问题;D 但是,如果 JVM 认为它是值得的,那么它只会优化和内联这个方法,例如它经常被循环调用。但是(查看您的最后一个答案)JITing 可能会有所不同,具体取决于平台(SPARC/x86 等)
【解决方案2】:

默认编译器阈值为 10000。-XX:CompilerThreshold= 这意味着方法或循环(对于服务器 JVM)必须至少调用 10000 次才能编译为本机代码。

编译后它可以被内联,但是调用堆栈确实显示了这一点。知道内联代码来自另一个方法并且您永远不会看到截断的调用堆栈是足够聪明的。

分析器尝试示例代码并分配时间。它并不总是做得很好,而且你得到的方法显然不是时间消费者被分配 CPU 时间。 VisualVM 是一个免费的分析器,它在 Java 中实现。如果您使用像 YourKit 这样的分析器,您可以获得更准确的结果,因为它使用本机代码,例如不会产生垃圾。

【讨论】:

    【解决方案3】:

    从我读过的关于旧版本 JVM 的文献中,通过声明方法 final 将确定它内联转换该方法。 现在您不必将该方法指定为 final 来优化代码。

    您应该让 JVM 优化代码,并且仅当您明确不希望覆盖该方法时才使该方法成为最终方法。考虑到应用程序的其余代码,JVM 可能不会使您的方法内联,因为它自己的优化更快。

    【讨论】:

      【解决方案4】:

      经过大量实验,我无法让 Sun JDK 6 在接口上调用时内联此方法。

      幸运的是,所涉及的呼叫站点数量有限,并且不断变化

      public void paint(Shape shape) {
        if(shape.checkFlag()) { /* do stuff */ }
      } 
      

      public void paint(Shape shape) {
        if(((AbstractShape)shape).checkFlag()) { /* .. */ }
      }
      

      足以让 JVM 内联该方法。与最初的 6 分钟运行时间相比,相关计算的运行时间减少了 13%。

      【讨论】:

        猜你喜欢
        • 2019-02-10
        • 1970-01-01
        • 1970-01-01
        • 2022-01-03
        • 1970-01-01
        • 2013-11-04
        • 2016-10-30
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多