【问题标题】:What kind of optimalization JIT applied to while loop什么样的优化JIT应用于while循环
【发布时间】:2020-04-13 20:27:58
【问题描述】:

我写下了这段代码:

public class Main {

    private boolean stopThread = false;
    private int counter = 0;

    public static void main(String[] args) throws InterruptedException {
        final Main main = new Main();

        new Thread(() -> {
            try {
                System.out.println("Start");
                Thread.sleep(100);
                System.out.println("Done");
                main.stopThread = true;
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        }).start();

        new Thread(() -> {
            while (!main.stopThread) {
                main.counter++;
            }
            System.out.println(main.counter);
        }).start();
        System.out.println("End");
    }
}

当我运行它时,while 循环将永远运行。我在这方面有点挣扎,我对这种代码应用什么样的优化 JIT 感到困惑。

首先我认为这是stopThread 变量的可见性问题,但即使这是真的while 循环应该比我将stopThread 分配给true 稍晚一点(当来自第一个线程的 CPU 缓存被刷新到主内存时),所以情况并非如此。它看起来像 JIT 硬编码 falsestopThread 变量,如果是真的,为什么这个变量没有在运行时以某种方式定期刷新?

显然,volatile 关键字解决了这个问题,但它并没有回答我这个问题,因为volatile 可以确保可见性并防止 JIT 进行优化次数。

更重要的是,当我将 sleep 时间更改为 1 毫秒时,第二个线程将正确终止,所以我很确定这与变量可见性无关。

更新:值得一提的是,当 sleep 时间设置为 1-10 毫秒时,我从 counter 获得了非零值。

更新 2:另外,我可以说 -XX:+PrintCompilation 表明当 sleep 时间设置为 100 毫秒时,while 循环被编译,On Stack Replacement发生了。

更新 3:可能这就是我想要的:https://www.youtube.com/watch?v=ADxUsCkWdbE&feature=youtu.be&t=889。正如我所想 - 这是 JIT 执行的“优化”之一,防止它的方法是将变量指定为 volatile,或将 loadloadFence 作为 while 循环的第一行。

回答:正如@a​​pangin 所说:

这种优化是循环不变提升。允许 JIT 将 stopThread 的负载移出循环,因为它可能假设非易失性字段在外部没有变化,并且 JIT 还看到 stopThread 在循环内部没有变化。

【问题讨论】:

  • 这个优化是Loop invariant hoisting。允许 JIT 将 stopThread 的负载移出循环,因为它可能假设非易失性字段不会在外部发生变化,并且 JIT 还可以看到 stopThread 在循环内没有变化。
  • @apangin 可能这就是为什么loadLoadFance() 也可以工作,而无需将变量指定为volatile。也许仍然需要volatile 来防止 CPU 缓存中的值过时。谢谢。
  • @apangin,您可以将其添加为答案,因为当前答案未提及此处发生的确切优化。

标签: java performance jvm jit


【解决方案1】:

没有任何同步或volatile关键字,它允许不从内存中读取stopThread变量,它可以从它第一次放置的缓存(甚至寄存器)中读取它。

它究竟做了什么以及为什么它与 sleep(1) 一起工作,我们只能推测。我的猜测是第一个线程在第二个线程读取它之前写了 stopThread = true 。也许先尝试启动循环线程。

【讨论】:

  • 如果你回答的第二部分是真的,我不会从counter 得到任何结果,但我会从中得到非零值。
  • 可能前几次迭代的执行方式与其他迭代不同(JVM 可以解释代码,然后 JIT 运行它的编译版本)并且可能(这是疯狂的猜测)就是这样。
  • 我同意@Alpedar 的观点。即使在代码已经编译/执行之后,JVM 也会优化/重新编译代码。这就是为什么 java 代码的性能测试一次完成时不准确的主要原因(因此为什么大多数测试设置使用循环,它们跳过第一块迭代,排除/跳过“优化阶段”)。一个非常大的变化也是造成“无法解释”的结果的原因。
【解决方案2】:

原因不是 JIT 优化。您的代码正在对共享变量stopThread(我们称之为“标志”)进行非同步访问。

在将标志设置为真值的线程和检查该值的另一​​个线程之间基本上存在竞争条件。如果在进入循环之前标志设置为真,则代码完成。如果不是(比赛丢失),循环将无限长地继续,因为 CPU 缓存保存错误值。当标志为volatile时,它的值是从主内存而不是CPU缓存中读取的,并且循环最终在标志设置线程完成睡眠后立即结束。

【讨论】:

  • 但如果这是真的:If the flag is set true before the loop is entered, the code completes. 我不会从counter 获得任何值,但是当sleep 时间设置为时我会得到非零值1 毫秒。
  • 我使用sleep(1)stopThreadvolatile 对您的代码进行了测试,结果似乎完全一样。 Thread.sleep()native 方法,这意味着实现取决于操作系统(和处理器架构),但我对 Windows/Intel i7 的测试似乎暗示当您指定最小线程时不会使用 CPU 缓存睡觉时间。可能他们甚至将Thread.sleep(0)Thread.sleep(1) 视为Windows 中的“无睡眠”,作为某种优化。使用 2 毫秒的睡眠时间总是会挂起应用程序。
  • 简而言之,这种现象似乎与 CPU 缓存和 Java 的内存模型有关,而不是 JIT。
  • 我在 10 毫秒 sleep 上对其进行了测试,应用程序在大多数尝试中都会挂起,但并非总是。但我发现了很棒的话题,可能这就是我想要的:youtu.be/ADxUsCkWdbE?t=889。正如我所想 - 这是 JIT 执行的“优化”之一,防止它的方法是将变量指定为 volatile
  • 当然可以质疑任何答案,但有时我的想法是人们提出问题,然后当他们得到不适合的答案时开始捍卫他们最初持有的观点他们的预定义模型。如果您甚至不打算从不同的角度探索或研究这个问题,那么首先提出这个问题有什么意义呢?为什么这两种观点甚至会相互排斥?
【解决方案3】:

我认为你是从错误的方向来解决这个问题的。

您所拥有的是一个在内存可见性方面具有未指定行为的程序。如果两个线程正在读取和写入一个共享变量,则必须以特定方式完成,以保证一个线程看到其他线程写入。您需要使用synchronizedvolatile 变量来确保写入发生在读取之前。

如果 happen before 不存在,则允许 JVM 假设只有当前线程正在访问/更新变量。 JIT 编译器实际上做什么取决于各种各样的事情,一般来说详细说明没有帮助。

还值得注意的是,volatile 不一定是内存可见性缺陷的最佳(最有效)修复方法。

参考资料:

【讨论】:

  • 看来,VarHandle.loadLoadFance() 也可以解决我认为比volatile 变量更有效的问题。
猜你喜欢
  • 1970-01-01
  • 2020-10-19
  • 1970-01-01
  • 1970-01-01
  • 2014-10-09
  • 2016-03-18
  • 2023-03-18
  • 2021-05-06
相关资源
最近更新 更多