乐趣区

深入理解编译优化之循环展开和粗化锁

简介

之前在讲 JIT 的时候,有提到在编译过程中的两种优化循环展开和粗化锁,今天我们和小师妹一起从 Assembly 的角度来验证一下这两种编译优化方法,快来看看吧。

循环展开和粗化锁

小师妹:F 师兄,上次你讲到在 JIT 编译的过程中会进行一些编译上面的优化,其中就有循环展开和粗化锁。我对这两种优化方式很感兴趣,能不能展开讲解一下呢?

更多精彩内容且看:

  • 区块链从入门到放弃系列教程 - 涵盖密码学, 超级账本, 以太坊,Libra, 比特币等持续更新
  • Spring Boot 2.X 系列教程: 七天从无到有掌握 Spring Boot- 持续更新
  • Spring 5.X 系列教程: 满足你对 Spring5 的一切想象 - 持续更新
  • java 程序员从小工到专家成神之路(2020 版)- 持续更新中, 附详细文章教程

当然可以,我们先来回顾一下什么是循环展开。

循环展开就是说,像下面的循环遍历的例子:

for (int i = 0; i < 1000; i++) {x += 0x51;}

因为每次循环都需要做跳转操作,所以为了提升效率,上面的代码其实可以被优化为下面的:

for (int i = 0; i < 250; i++) {x += 0x144; //0x51 * 4}

注意上面我们使用的是 16 进制数字,至于为什么要使用 16 进制呢?这是为了方便我们在后面的 assembly 代码中快速找到他们。

好了,我们再在 x += 0x51 的外面加一层 synchronized 锁,看一下 synchronized 锁会不会随着 loop unrolling 展开的同时被粗化。

for (int i = 0; i < 1000; i++) {synchronized (this) {x += 0x51;}
 }

万事具备,只欠我们的运行代码了,这里我们还是使用 JMH 来执行。

相关代码如下:

@Warmup(iterations = 10, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Fork(value = 1,
        jvmArgsPrepend = {
        "-XX:-UseBiasedLocking",
                "-XX:CompileCommand=print,com.flydean.LockOptimization::test"
}
        )
@State(Scope.Benchmark)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class LockOptimization {

    int x;
    @Benchmark
    @CompilerControl(CompilerControl.Mode.DONT_INLINE)
    public void test() {for (int i = 0; i < 1000; i++) {synchronized (this) {x += 0x51;}
        }
    }

    public static void main(String[] args) throws RunnerException {Options opt = new OptionsBuilder()
                .include(LockOptimization.class.getSimpleName())
                .build();
        new Runner(opt).run();}
}

上面的代码中,我们取消了偏向锁的使用:-XX:-UseBiasedLocking。为啥要取消这个选项呢?因为如果在偏向锁的情况下,如果线程获得锁之后,在之后的执行过程中,如果没有其他的线程访问该锁,那么持有偏向锁的线程则不需要触发同步。

为了更好的理解 synchronized 的流程,这里我们将偏向锁禁用。

其他的都是我们之前讲过的 JMH 的常规操作。

接下来就是见证奇迹的时刻了。

分析 Assembly 日志

我们运行上面的程序,将会得到一系列的输出。因为本文并不是讲解 Assembly 语言的,所以本文只是大概的理解一下 Assembly 的使用,并不会详细的进行 Assembly 语言的介绍,如果有想深入了解 Assembly 的朋友,可以在文后留言。

分析 Assembly 的输出结果,我们可以看到结果分为 C1-compiled nmethod 和 C2-compiled nmethod 两部分。

先看 C1-compiled nmethod:

第一行是 monitorenter, 表示进入锁的范围,后面还跟着对于的代码行数。

最后一行是 monitorexit, 表示退出锁的范围。

中间有个 add $0x51,%eax 操作,对于着我们的代码中的 add 操作。

可以看到 C1—compiled nmethod 中是没有进行 Loop unrolling 的。

我们再看看 C2-compiled nmethod:

和 C1 很类似,不同的是 add 的值变成了 0x144, 说明进行了 Loop unrolling,同时对应的锁范围也跟着进行了扩展。

最后看下运行结果:


Benchmark              Mode  Cnt     Score     Error  Units
LockOptimization.test  avgt    5  5601.819 ± 620.017  ns/op

得分还不错。

禁止 Loop unrolling

接下来我们看下如果将 Loop unrolling 禁掉,会得到什么样的结果。

要禁止 Loop unrolling,只需要设置 -XX:LoopUnrollLimit= 1 即可。

我们再运行一下上面的程序:

可以看到 C2-compiled nmethod 中的数字变成了原本的 0x51,说明并没有进行 Loop unrolling。

再看看运行结果:

Benchmark              Mode  Cnt      Score      Error  Units
LockOptimization.test  avgt    5  20846.709 ± 3292.522  ns/op

可以看到运行时间基本是优化过后的 4 倍左右。说明 Loop unrolling 还是非常有用的。

总结

本文介绍了循环展开和粗化锁的实际例子,希望大家能够喜欢。

本文的例子 https://github.com/ddean2009/learn-java-base-9-to-20

本文作者:flydean 程序那些事

本文链接:http://www.flydean.com/jvm-jit-loop-unrolling-lock-coarsening/

本文来源:flydean 的博客

欢迎关注我的公众号: 程序那些事,更多精彩等着您!

退出移动版