JIT反优化,原因="约束"。为什么JIT要反优化方法?

6

有人可以指点我找到一个方向,以便理解为什么JIT会撤销我的循环(OSR)吗?它似乎被C1编译一次,然后被反编译多次(我可以看到数十个或者可能是数百个以<deoptimized...>开头的日志)。

这是包含那个重要循环的类:

@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;
    }
}

即时编译日志:
 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. 为什么会有如此多关于反优化的日志,而不只是一个?看起来它只被编译一次,但被反编译多次。
3个回答

7
我很高兴看到 @aran 的建议对您有所帮助,但这只是一个幸运的巧合。毕竟,JIT内联选项影响许多事情,包括编译顺序、时间等等。实际上,去优化(deoptimization)与内联无关。
我能够重现您的问题,以下是我的分析。
我们在 HotSpot sources 中看到 <deoptimized> 消息是由 Deoptimization::deoptimize_single_frame 函数打印的。让我们使用 async-profiler 找出调用此函数的位置。为此,请添加以下JVM选项:
-agentlib:asyncProfiler=start,event=Deoptimization::deoptimize_single_frame,file=deopt.html

这是输出的相关部分:

deoptimize_single_frame

因此,取消优化的原因是 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被第4层编译所取代,一切都变得正常起来。
当然,这通常不应该发生。这实际上是一个JVM错误JDK-8253118。它已在JDK 16中修复,并且可能会被回溯到JDK 11u。我已验证,在JDK 16 Early-Access版本中不会发生过度反优化。

2

剧透警告

正如@apangin所说,这是一次幸运的尝试。如果你想知道真正发生了什么,请不要浪费时间阅读本答案。

![我回答这个问题时的照片 -- 我回答这个问题时的照片


虽然JIT编译器在某些情况下可能会非常积极地进行内联,但它仍然有自己的时间限制,如果内联需要花费大量时间,它将不会内联。因此,只有当方法的字节码大小小于35个字节时,它才有资格进行内联默认情况下)。

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

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

Java Performance: The Definitive Guide by Scott Oaks

关于是否内联方法的基本决策取决于它有多热门和大小。JVM根据内部计算确定一个方法是否热门(即经常被调用);它不会直接受到任何可调参数的影响。如果一个方法因为经常被调用而有资格进行内联,那么只有当它的字节码大小小于325个字节(或者指定为-XX:MaxFreqInlineSize=N标志)时,它才会被内联。否则,它只有在很小的情况下才有资格进行内联:小于35个字节(或者指定为-XX:MaxInlineSize=N标志)。

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

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

对于读者-上面的建议“修复”了问题(实际上没有真正修复)。怎么做的?没有头绪。我甚至被给予了正确的答案勾勾lol


附录

我只是把这里放着,因为看起来很酷

-- JVM语言的工作负载特征

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

enter image description here

这段内容似乎是一些人名和一个图片链接。

1
你是对的,谢谢!如果我设置“-XX:MaxInlineSize=90”,它可以正常工作。所以我必须让我的方法变小一点。关于线程 - 有相同的线程='132773'(数字),所以我猜它们是相关的。 - minizibi
2
但是反过来,JIT 显然没有确定这些方法调用是热点。因此,内联它们的性能优势可能会比您预期(或希望)的要小。我想知道如果你只是让 JIT 做它的事情是否会更好。你做了一些应用级别的基准测试和分析吗?这是否告诉你这些方法实际上是性能热点? - Stephen C
2
答案令人困惑 - 似乎对去优化的理解有误。 "去优化" 不同于 "次优编译"。而且与内联无关。 - apangin
1
@aran 实际上,内联是最重要的优化。但这个问题涉及到优化,这是一个不同的概念。没有“因为JIT没有内联你的方法”这样的反优化原因。我不知道你为什么得出这样的结论,但你的第一句话似乎不正确,因此答案的其余部分(仅仅与内联相关)也是无关紧要的。但我不介意,因为它在某种程度上还是有帮助的 :) - apangin
1
是的,在这种情况下,需要进行反优化以从不太优化的代码切换到更优化的代码。它通过一个短暂的中间步骤完成:C1->解释器->C2。正如 OP 正确地指出的那样,这种反优化应该只在单个方法中发生一次。实际上,由于提到的 JVM 错误,它会多次发生。 - apangin
显示剩余12条评论

0

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

  1. 过多的deopts并不总是发生在aarch64上,但在我测试的x86机器上从未发生;
  2. 这个问题导致了运行时巨大的变化(在我的情况下超过30%),在过多的deopts发生和不发生之间;
  3. 应用@apagin提到的补丁后,过多的deopts问题消失了,性能也变得稳定。然而,在应用补丁之前的最高性能水平下,它比最高性能水平低。你在应用补丁之前检查过补丁是否会导致性能损失吗?@apagin。

1
根据更多的测试,即使应用了补丁程序,大规模的退化仍然存在。 - alan

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接