JIT 去优化,原因="constraint"。为什么 JIT 取消优化方法?

JIT deoptimization, reason="constraint". Why JIT deoptimizes method?

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

这是包含重要循环的 class:

@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. 为什么反优化的日志那么多,一个也没有?貌似编译一次,反编译多次

剧透

正如@apangin 评论的那样,这是一个幸运的镜头。如果您想知道到底发生了什么,请不要浪费时间阅读此答案。

-- 我回答时的照片


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

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

<jvms bci='37' method='Worker run ()V' bytes='81' ...

Java Performance: The Definitive Guide by Scott Oaks

The basic decision about whether to inline a method depends on how hot it is and its size. The JVM determines if a method is hot (i.e., called frequently) based on an internal calculation; it is not directly subject to any tunable parameters. If a method is eligible for inlining because it is called frequently, then it will be inlined only if its bytecode size is less than 325 bytes (or whatever is specified as the -XX:MaxFreqInlineSize=N flag). Otherwise, it is eligible for inlining only if it is small: less than 35 bytes (or whatever is specified as the -XX:MaxInlineSize=N flag).

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

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

致 reader - 上面的建议“解决”了这个问题(没有真正解决)。如何?不知道。我什至得到了正确答案 tick lol


附录

(我就把这个放在这里吧,因为看起来很酷)

-- Workload characterization of JVM languages

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

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

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

我们在HotSpot sources that <deoptimized> message is printed by Deoptimization::deoptimize_single_frame function. Let's engage 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 方法。基本上,它取消优化 Worker.run 在第 3 层编译的框架,并用完全相同的方法替换它!这一次又一次地重复,直到 C2 完成其后台作业。然后Worker.run换成tier 4编译,一切就OK了

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

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

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