【问题标题】:JIT deoptimization, reason="constraint". Why JIT deoptimizes method?JIT 去优化,reason="constraint"。为什么 JIT 会去优化方法?
【发布时间】:2021-05-06 03:59:33
【问题描述】:

有人能指出我的方向吗?这可能会让我明白为什么 JIT 会去优化我的循环? (OSR)。看起来它被 C1 编译一次,然后多次去优化(我可以看到几十个或几百个以 开头的日志)

这是包含重要循环的类:

@SynchronizationRequired
public class Worker implements Runnable
{
    private static final byte NOT_RUNNING = 0, RUNNING = 1, SHUTDOWN = 2, FORCE_SHUTDOWN = 3;
    private static final AtomicIntegerFieldUpdater<Worker> isRunningFieldUpdater =
            AtomicIntegerFieldUpdater.newUpdater(Worker.class, "isRunning");

    private volatile int isRunning = NOT_RUNNING;

    private final Queue<FunkovConnection> tasks = new SpscUnboundedArrayQueue<>(512);

    /**
     * Executing tasks from queue until closed.
     */
    @Override
    public void run()
    {
        if (isRunning())
        {
            return;
        }

        while (notClosed())
        {
            FunkovConnection connection = tasks.poll();
            if (null != connection)
            {
                connection.run();
            }
        }

        if (forceShutdown())
        {
            setNonRunning();
            return;
        }

        FunkovConnection connection;
        while ((connection = tasks.poll()) != null)
        {
            connection.run();
        }

        setNonRunning();
    }

    public void submit(FunkovConnection connection)
    {
        tasks.add(connection);
    }


    /**
     * Shutdowns worker after it finish processing all pending tasks on its queue
     */
    public void shutdown()
    {
        isRunningFieldUpdater.compareAndSet(this, RUNNING, SHUTDOWN);
    }

    /**
     * Shutdowns worker after it finish currently processing task. Pending tasks on queue are not handled
     */
    public void shutdownForce()
    {
        isRunningFieldUpdater.compareAndSet(this, RUNNING, FORCE_SHUTDOWN);
    }

    private void setNonRunning()
    {
        isRunningFieldUpdater.set(this, NOT_RUNNING);
    }

    private boolean forceShutdown()
    {
        return isRunningFieldUpdater.get(this) == FORCE_SHUTDOWN;
    }

    private boolean isRunning()
    {
        return isRunningFieldUpdater.getAndSet(this, RUNNING) == RUNNING;
    }

    public boolean notClosed()
    {
        return isRunningFieldUpdater.get(this) == RUNNING;
    }
}

JIT 日志:

 1. <task_queued compile_id='535' compile_kind='osr' method='Worker run ()V' bytes='81' count='1' backedge_count='60416' iicount='1' osr_bci='8' level='3' stamp='0,145' comment='tiered' hot_count='60416'/>
 2. <nmethod compile_id='535' compile_kind='osr' compiler='c1' level='3' entry='0x00007fabf5514ee0' size='5592' address='0x00007fabf5514c10' relocation_offset='344' insts_offset='720' stub_offset='4432' scopes_data_offset='4704' scopes_pcs_offset='5040' dependencies_offset='5552' nul_chk_table_offset='5560' oops_offset='4624' metadata_offset='4640' method='Worker run ()V' bytes='81' count='1' backedge_count='65742' iicount='1' stamp='0,146'/>
 3. <deoptimized thread='132773' reason='constraint' pc='0x00007fabf5515c24' compile_id='535' compile_kind='osr' compiler='c1' level='3'>
<jvms bci='37' method='Worker run ()V' bytes='81' count='1' backedge_count='68801' iicount='1'/>
</deoptimized>
4. <deoptimized thread='132773' reason='constraint' pc='0x00007fabf5515c24' compile_id='535' compile_kind='osr' compiler='c1' level='3'>
<jvms bci='37' method='Worker run ()V' bytes='81' count='1' backedge_count='76993' iicount='1'/>
</deoptimized>
5.<deoptimized thread='132773' reason='constraint' pc='0x00007fabf5515c24' compile_id='535' compile_kind='osr' compiler='c1' level='3'>
<jvms bci='37' method='Worker run ()V' bytes='81' count='1' backedge_count='85185' iicount='1'/>
</deoptimized>
6. <deoptimized thread='132773' reason='constraint' pc='0x00007fabf5515c24' compile_id='535' compile_kind='osr' compiler='c1' level='3'>
<jvms bci='37' method='Worker run ()V' bytes='81' count='1' backedge_count='93377' iicount='1'/>
</deoptimized>

这里有两个问题:

  1. 取消优化的原因可能是什么? “约束”对我来说似乎没有什么意义
  2. 为什么有这么多关于去优化的日志,而不是一个?它看起来像是编译了一次但反编译了多次

【问题讨论】:

    标签: java jvm jit


    【解决方案1】:

    我很高兴@aran 的建议对您的情况有所帮助,但是,这只是一个幸运的巧合。毕竟,JIT 内联选项会影响很多事情,包括编译顺序、时序等。事实上,反优化与内联无关。

    我能够重现您的问题,这是我的分析。

    我们在HotSpot sources 中看到&lt;deoptimized&gt; 消息由Deoptimization::deoptimize_single_frame 函数打印。让我们联系async-profiler 来查找调用此函数的位置。为此,请添加以下 JVM 选项:

    -agentlib:asyncProfiler=start,event=Deoptimization::deoptimize_single_frame,file=deopt.html
    

    这是输出的相关部分:

    所以,去优化的原因是Runtime1::counter_overflow函数。由 C1 在第 3 层编译的方法,计算调用和后向分支(循环迭代)。每 2Tier3BackedgeNotifyFreqLog 迭代,一个方法调用 Runtime1::counter_overflow 来决定是否应该在更高层重新编译。

    在您的日志中,我们看到 backedge_count 正好递增 8192 (213),索引 37 处的字节码是 goto,对应于 while (notClosed()) 循环。

    <jvms bci='37' method='Worker run ()V' bytes='81' count='1' backedge_count='76993' iicount='1'/>
    <jvms bci='37' method='Worker run ()V' bytes='81' count='1' backedge_count='85185' iicount='1'/>
    <jvms bci='37' method='Worker run ()V' bytes='81' count='1' backedge_count='93377' iicount='1'/>
    

    当计数器溢出时(每 8192 次迭代),JVM 检查给定字节码索引的 OSR 编译方法是否准备好(它可能还没有准备好,因为 JIT 编译在后台运行)。但是如果 JVM 找到这样的方法,它会通过对当前帧进行反优化并用相应的 OSR 方法替换它来执行 OSR 转换。

    事实证明,在您的示例中,JVM 找到了在第 3 层编译的现有 OSR 方法。基本上,它取消了在第 3 层编译的 Worker.run 帧,并用完全相同的方法替换它!这一次又一次地重复,直到 C2 完成其后台工作。然后Worker.run换成四层编译,一切都变好了。

    当然,这通常不应该发生。这实际上是一个 JVM 错误JDK-8253118。它已在 JDK 16 中修复,并且可能会向后移植到 JDK 11u。我已验证 JDK 16 Early-Access 版本不会发生过度的去优化。

    【讨论】:

      【解决方案2】:

      剧透

      这是一个幸运的镜头,就像@apangin cmets。如果您想知道真正发生了什么,请不要浪费时间阅读此答案。

      -- 我在回答时的照片


      虽然 JIT 编译器在某些情况下可能会积极内联,但它仍然有自己的时间限制,如果需要大量时间来完成,它不会内联。因此,方法只有在其字节码大小小于 35 字节时才可以内联默认情况下)。

      在您的情况下,您的方法大小为 81 字节,因此不符合条件:

      &lt;jvms bci='37' method='Worker run ()V' bytes='81' ...

      Java Performance: The Definitive Guide by Scott Oaks

      是否内联方法的基本决定取决于热度 它是和它的大小。 JVM 确定一个方法是否是热的(即, 频繁调用)基于内部计算;它不是 直接受制于任何可调参数。如果方法符合条件 for inlining 因为被频繁调用,那么就会被内联 仅当其字节码大小小于 325 字节(或任何 指定为 -XX:MaxFreqInlineSize=N 标志)。 否则就是 仅当它很小时才符合内联条件:小于 35 字节(或 指定为 -XX:MaxInlineSize=N 标志的任何内容)。

      为了使您的方法内联,您可以通过命令行更改内联大小限制,指定 -XX:MaxInlineSize=N

      作为测试,您可以尝试指定类似 -XX:MaxInlineSize=90 以检查这些方法现在是否已内联。

      致读者 - 上面的建议“修复”了问题(没有真正修复)。如何?不知道。我什至得到了正确的答案打勾哈哈


      附录

      我就让它在这里看起来很酷

      -- Workload characterization of JVM languages

      Aibek S. Lukas S. Lubomír B. Andreas S. Andrej P. Yudi Z. Walter B.

      【讨论】:

      • 你是对的,谢谢!如果我设置-XX:MaxInlineSize=90,它可以正常工作。所以我必须让我的方法更小。关于线程 - 有相同的 thread='132773' (number) 所以我猜它与同一个线程有关
      • 但另一方面是 JIT 显然没有确定这些方法调用是热的。因此,内联它们的性能优势可能低于您的预期(或希望)。我想知道如果你让 JIT 做它的事情会不会更好。您是否进行了一些应用程序级别的基准测试和分析?这是否告诉您这些方法实际上是性能热点?
      • @StephenC 是的,考虑相同。此外,文档建议在更改默认内联最大大小时要小心。这就像嘿,如果我们这样做,是有原因的:内联这些方法的成本可能无法补偿获得的可能优化。我会更新答案以指出这一点,感谢斯蒂芬的评论
      • @aran - 我对基准测试等的建议确实是针对 minizibi 的。作为一项规则,在深入研究优化或调整应用程序代码的特定方法之前,应该进行基准测试/分析。
      • 答案令人困惑——似乎误解了去优化是什么。 “反优化”与“次优编译”不同。而且它与内联无关。
      【解决方案3】:

      我最近在 aarch64 平台上遇到了同样的问题。一些有趣的发现:

      1. 过度的 deopts 在 aarch64 上并非一直发生,但在我测试的 x86 机器上却从未发生过;
      2. 此问题会导致过度 deopt 发生与未发生之间的巨大运行间差异(在我的情况下 > 30%);
      3. 应用@apagin 提到的补丁后,deopts 过多的问题消失了,性能变得稳定了。但是,它低于应用补丁之前的最高性能。您之前是否检查过补丁是否会导致性能损失? @apagin。

      【讨论】:

      • 根据更多测试应用补丁似乎仍然存在大量deopts
      猜你喜欢
      • 1970-01-01
      • 2011-08-01
      • 1970-01-01
      • 2015-12-25
      • 2011-12-12
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-11-26
      相关资源
      最近更新 更多