【问题标题】:Java Null check elimination can cause bad code?Java Null 检查消除会导致坏代码?
【发布时间】:2018-02-13 01:17:31
【问题描述】:

最近,我阅读了 here 关于 JVM 优化的文章,这很棒,
但我有一个优化问题,即 Null Check Elimination(或 Uncommon 陷阱)

总结 Null Check Elimination 删除 if (obj == null) ... 并希望最好,而如果遇到 Segmentation Fault 它会重新编译代码,这次包括被忽略的代码if (obj == null) ....

我的问题是,鉴于:

bool foo(MyClass obj)
{
   if(obj == null)
      return false;
   m_someVar++;
   obj.doSomething(m_someVar);
}

因为 Null-Check 已被消除,并且仅在 m_someVar++ 之后才需要。
foo 会在 c 为空时多执行一次 m_someVar++ 吗?

编辑:

是否有一些资源可以更深入地解释这种优化的实现,解释这种优化如何使代码在语义上保持一致?

谢谢

【问题讨论】:

  • 你测试过会发生什么吗?
  • 还有其他一些优化,例如更改 if-then-else 订单等,可能会产生与您描述的效果类似的效果,所以我认为这已经涵盖(虽然没有测试过)。正如 Carcigenicate 建议的那样,进行测试并自己找出答案。
  • 您可以确定,Java HotSpot JIT 编译器所做的所有优化都不会影响语言语义。因此,在您的情况下,它可能不会应用 Null-Check Elimination,或将 ++ 延迟到修改后的 obj.doSomething(m_someVar+1) 或在分段错误后执行 m_someVar--
  • 是的,它似乎在汇编程序级别执行了两次命令。但它不会破坏最终结果(否则将是不正确的 JVM 实现)。看看这篇文章jcdav.is/2015/10/06/SIGSEGV-as-control-flow这家伙逐行分析C2输出的情况完全一样。
  • @RalfKleberhoff 谢谢,我很确定它会,但不知道 JVM 如何知道“中间代码”何时对运行两次无动于衷。是否有一些来源可以对实现进行更深入的解释?

标签: java optimization jvm null-check


【解决方案1】:

它认为你误解了空检查消除的真正含义。

实际上,它是关于消除(例如)在实例上调用方法时隐含的 null 检查。

在您的示例中,显式 if(obj == null) 不会被消除。这将(如您所见)改变程序的行为。消除该空检查将是不正确的优化。

但是,在 HERE 处的隐式 null 检查如下:

bool foo(MyClass obj)
{
   if(obj == null)
      return false;
   m_someVar++;
   obj.doSomething(m_someVar);  // HERE
}

可以消除,因为 JIT 编译器应该能够推断出 obj 在代码到达该点时将始终为非空值。 (不过,这是一种不同的空值检查优化。)


是否有一些资源可以更深入地解释这种优化的实现...

我不相信。至少,没有什么是确定的。 (除非您在 JIT 编译器源代码中计算可能的 cmets!)

未指定 JIT 优化器行为。只要不违反 JLS 中定义的 Java 语义,它基本上可以做任何事情。

...这解释了这种优化如何使代码在语义上保持不变?

正如我所解释的,您正在阅读的乐观空值检查消除优化不适用于您的示例。 (它根据 JLS 使代码行为不正确。)

【讨论】:

    【解决方案2】:

    有很多方法可以避免这个问题:评论中已经提到延迟或撤消增量。 JVM 有一大堆技巧,其中一些适用:

    • obj.doSomething(m_someVar) 是非虚拟方法调用(privatefinal 方法,或从不被覆盖的方法)时,它可能需要显式的空检查(*),因为不会有 SEGV,但是必须在调用之前抛出 NPE。我假设该方法未内联,否则应在内联后应用此分析。
    • obj.doSomething(m_someVar) 是一个虚方法调用时,你必须进行“方法分派”,即类似obj.getClass().getPointerToMethod("doSomething(int)") 的操作,以确定要调用的具体方法。我写了“类似的东西”,因为这样做确实非常耗时,并且会尽可能地优化。如果obj 恰好是null,则此调度可以移动到增量之上,并且它本身会抛出 NPE。
    • 如果你很幸运,那么调度可能看起来像if (obj.getClass() != MyClass.class) uncommon_trap();,这是最简单的情况(称为“单态调用站点”),但即使这样也涉及取消引用obj,并且你再次获得@987654330 的NPE @。

    (*) 空检查可能看起来像obj.getClass(),它在汇编程序中是从相对于obj 指针的固定偏移量加载的单条指令。当obj == null 出现未映射的页面时。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2011-12-08
      • 2011-11-26
      • 1970-01-01
      • 1970-01-01
      • 2018-06-03
      • 2013-08-11
      • 1970-01-01
      相关资源
      最近更新 更多