【问题标题】:Java: How will JVM optimise the call to a void and empty function?Java:JVM 将如何优化对 void 和 empty 函数的调用?
【发布时间】:2013-01-08 02:37:36
【问题描述】:

假设我们有以下类:

public class Message extends Object {}

public class Logger implements ILogger {
 public void log(Message m) {/*empty*/}
}

以及以下程序:

public static void main(String args[]) {
  ILogger l = new Logger();
  l.log((Message)null); // a)
  l.log(new Message()); // b)
}

Java 编译器会去掉语句 ab 吗?在这两种情况下(剥离或不剥离),Java 编译器做出决定的理由是什么?

【问题讨论】:

  • 在接口stackoverflow.com/a/2814831/24396前面使用I
  • 它可能会内联导致很少或没有代码的空方法。
  • 如果你的 main 方法不在 Logger 类编译器不应该优化它,因为 Logger 类可以稍后重新编译,这次使用 log 方法中的代码。
  • 您对 log() 的第二次调用可能永远不会得到优化,因为 Message() 构造函数可能会产生副作用。

标签: java jvm javac


【解决方案1】:

Java 编译器会删除语句 ab 吗?

javac(源到字节码)编译器不会剥离任何一个调用。 (通过检查字节码很容易检查这一点;例如,查看javap -c 输出。)

在这两种情况下(剥离或不剥离),Java 编译器做出决定的理由是什么?

符合 JLS :-)。

从务实的角度:

  • 如果 javac 编译器优化了调用,Java 调试器将根本无法看到它们......这会让开发人员感到困惑。
  • 如果 Message 类和主类是独立编译/修改的,早期优化(javac)将导致损坏。例如,考虑这个序列:

    • Message已编译,
    • 主类编译完毕,
    • Message 被编辑,以便 log 做一些事情......并重新编译。

    现在我们有一个错误编译的主类,它在 ab 处没有做正确的事情,因为过早内联的代码已经过时了。


但是,JIT 编译器可能在运行时以多种方式优化代码。例如:

  • 如果 JIT 编译器可以推断不需要虚拟方法分派,则可以内联 ab 中的方法调用。 (如果 Logger 是应用程序使用的唯一实现 ILogger 的类,那么对于一个好的 JIT 编译器来说,这是显而易见的。)

  • 在内联第一个方法调用后,JIT 编译器可能会确定主体是 noop 并优化调用。

  • 在第二个方法调用的情况下,JIT 编译器可以进一步推断(通过转义分析)Message 对象不需要在堆上分配......甚至根本不需要。

(如果您想知道 JIT 编译器(在您的平台上)实际上做了什么,Hotspot JVM 有一个 JVM 选项,可以为选定的方法转储 JIT 编译的本机代码。)

【讨论】:

  • 那么,如果我在ab 中放置一个断点,是否可以保证JIT 编译器不会优化相应的语句?
  • 没错。在此处阅读“动态去优化”:oracle.com/technetwork/java/whitepaper-135217.html
  • @StephenC 谢谢,有道理,特别是这部分 “全速调试:Java HotSpot VM 利用动态去优化技术来支持全速调试应用程序。在早期的 Java 虚拟机中实现,启用调试支持后,应用程序仅在解释器中运行。..."
【解决方案2】:

反汇编以下文件(使用javap -c)表明它们在编译为字节码时不会被 1.7.0 编译器剥离:

public class Program
{
    public static class Message extends Object {}

    public interface ILogger {
        void log(Message m);
    }

    public static class Logger implements ILogger {
        public void log(Message m) { /* empty */ }
    }

    public static void main(String[] args) {
        ILogger l = new Logger();
        l.log((Message)null); // a)
        l.log(new Message()); // b)
    }
}

结果如下。关键位是第 13 行和第 26 行的调用。

Compiled from "Program.java"
public class Program {
  public Program();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: new           #2                  // class Program$Logger
       3: dup
       4: invokespecial #3                  // Method Program$Logger."<init>":()V
       7: astore_1
       8: aload_1
       9: aconst_null
      10: checkcast     #4                  // class Program$Message
      13: invokeinterface #5,  2            // InterfaceMethod Program$ILogger.log:(LProgram$Message;)V
      18: aload_1
      19: new           #4                  // class Program$Message
      22: dup
      23: invokespecial #6                  // Method Program$Message."<init>":()V
      26: invokeinterface #5,  2            // InterfaceMethod Program$ILogger.log:(LProgram$Message;)V
      31: return
}

编辑:但是,正如@mikera 指出的那样,JIT 编译器可能会在程序运行时进行进一步优化,这可能能够消除调用。不幸的是,我对细节了解得不够多,无法对此发表评论。

旁注:您可能对此链接感兴趣,该链接涉及 Hotspot JVM 使用的性能技术:

https://wikis.oracle.com/display/HotSpotInternals/PerformanceTechniques

【讨论】:

  • +1 用于进行反汇编,但请注意 JIT 编译器可能会在此阶段之后进行实际消除.....所以它实际上并不能证明这些调用是否得到最终优化了。
  • 其实javac编译器做了minimal优化。
【解决方案3】:

可能最终,绝对不是立即,也不一定永远。 JIT 不做任何保证,尤其是对于只被调用几次的方法。 (它可能会被简单地归类为 inlining log 调用,而内联代码恰好是......什么都没有。)

【讨论】:

    【解决方案4】:

    无法确定 - 这将取决于 JVM/Java 编译器的实现。

    足够聪明的编译器可以证明这两个语句都没有效果,因此可以消除它们。我相信大多数现代 JVM 都会这样做,尽管您需要对您的特定配置进行测试才能确定。

    a) 比 b) 更容易优化掉,因为 b) 包含一个构造函数调用,编译器还需要证明它没有副作用,然后才能优化掉整个语句。

    请注意,您希望这种消除是由 JIT 编译器而不是 Java 编译器本身完成的,即可能会生成包含日志函数调用的字节码,但这稍后会被 JIT 编译器优化掉当它编译为本机代码时。

    此外,由于 JIT 可以根据运行时统计信息等重新编译,因此代码可能会在那里开始,但在后续的优化中被编译掉。

    【讨论】:

      【解决方案5】:

      我不认为 java 编译器会删除调用,因为被调用的方法是空的,因为你以后也可以更改方法,而无需对 main 方法进行任何更改。

      【讨论】:

        【解决方案6】:

        如果您将引用设为 final,服务器 JIT 肯定会内联并最终完全消除代码: 最终 ILogger l = new Logger(); 在现代 JVM 中,大部分优化都是由 JIT 执行的。

        【讨论】:

          猜你喜欢
          • 2013-06-12
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2021-09-29
          • 1970-01-01
          相关资源
          最近更新 更多