哈,我发现我九个月前就写了草稿。 之所以没发,一定是因为我正要继续写下去,又崩溃了。
(难怪,macOS自带的中文输入法实在是太烦人了)
简单来说,原因是C2在编译OSR时未能进行足够的常量传播。 就这样。 关于缓存未命中等等的说法是完全不可靠的。
对主题的简要建议是:
在虚拟机上运行时,请记住不要只在 main() 中运行循环计时。 这是一个典型的错误。 重要的事情重复三遍:请用JMH,请用JMH,请用JMH。 除非你非常了解实现细节,否则在main中运行循环计时得到的结果对于普通程序员来说实际上是没有意义的,因为它们无法解释。 即使对于计时,使用 .() 也比 new Date().() 好得多。 如果要给常量指定别名,请习惯性地加上final以保证可靠性。 例如,如果这里的主要问题是写final int range = 8000; 你会发现使用range和第三级循环的条件一样快。 请多注意 -XX:+ 的输出。
只是酱。
首先,请了解OSR编译。 跳转至此传送门:OSR(On-Stack)的机制是怎样的?
提问者说,在内循环中使用文字8000比使用相同值的局部变量范围要快得多。 这是因为提问者在main中写了一个循环,这会触发OSR编译——只有JIT编译部分方法而不是整个方法。 main 示例中的局部变量范围只被赋值一次,但在 JIT 编译器看来,这次赋值并不在本次 OSR 编译覆盖的范围内,因此 JIT 编译器无法知道这个变量实际上是一个常量。 它只知道“哦,有这么一个变量,当进入OSR编译的代码时,它的值会从解释器传递过来”。 编译出来的代码质量会差很多。
当我在 Azul 工作时,我遇到一位客户提出了非常相似的问题。 他们还用了错误的方法来写和错误地解释它们。 当时的情况是OSR编译也被触发了,循环外定义的“实质性常量”并没有通过常量传播到OSR编译循环中。 循环内测试的是除法的性能。 常数值可以用一些位移、加法和减法来代替。 如果有不断的传播,分裂就可以专门化。 如果没有,就只能做普通的除法,而且速度会很慢。 。
当时,为了让客户满意,我实际上写了一个补丁,允许 C1 和 C2 在 OSR 编译下从循环外部传播更多常量。 代码有点复杂,解决的问题往往只是“有人又随机写了一遍”,所以没有合并到Azul Zing JVM,也没有提交到Azul Zing JVM。 知道问题之后,实现思路其实很简单。
一开始我提到添加final来修改局部变量。 这是Java语言规范层面的规定:被final修饰的局部变量,如果它的初始化表达式是常量表达式,那么这个局部变量也是常量表达式,它的常量值会被Java语言编译器识别(例如 javac 或 ECJ,而不是 JVM 中的 JIT 编译器)在以后的使用时传播和嵌入。 因此,如果最后修改是在range声明中加上的话,后面使用range的效果就会和直接手写8000一模一样。从JVM的角度来看,输入的字节码会是一模一样的,性能特征也会一模一样。当然是一模一样的。
至于问题的最后一个例子询问为什么当循环差1时性能这么差,这部分也与JIT编译的触发机制有关; 另外,这是最里面循环的1个差值,外面还有一个巨大的放大倍数。 。 如果提问者只想测试缓存性能,请使用JMH,以避免担心JVM的JIT编译系统的这么多实现细节。
如果提问者坚持用原代码继续实验,可以尝试在差异为1的例子中添加-XX:-,看看性能差异有多大。