JVM 解剖公园(19): 锁省略

2019 Java 开发者跳槽指南.pdf (吐血整理)….>>>

编译:ImportNew/唐尤华

shipilev.net/jvm/anatomy-quarks/19-lock-elision/

 

1. 写在前面

“JVM 解剖公园”是一个持续更新的系列迷你博客,阅读每篇文章一般需要5到10分钟。限于篇幅,仅对某个主题按照问题、测试、基准程序、观察结果深入讲解。因此,这里的数据和讨论可以当轶事看,不做写作风格、句法和语义错误、重复或一致性检查。如果选择采信文中内容,风险自负。

Aleksey Shipilёv,JVM 性能极客

推特 @shipilev

问题、评论、建议发送到 aleksey@shipilev.net"">aleksey@shipilev.net

2. 问题

据说加锁能避免 JVM 编译器佳化,所以如果代码中写有 synchronized JVM 就不能对这段代码优化,对吗?

3. 理论

Java 目前使用的内存模型中,未使用的锁不能保证在内存中产生效果。这意味着在非共享对象上同步是徒劳的,运行时在这里不会执行任何操作。虽然有可能,但不是必须优化,这就为优化留下了机会。

因此,如果逃逸分析发现对象不会逃逸,编译器可以消除同步。在实验中能观察到上述结论吗?

4. 实验

看看下面这个简单的 JMH 基准测试。我们在 new object 时对比了同步与非同步两种情况:

import org.openjdk.jmh.annotations.*;
@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Fork(3)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@State(Scope.Benchmark)
public class LockElision {
    int x;
    @Benchmark
    public void baseline() {
        x++;
    }
    @Benchmark
    public void locked() {
        synchronized (new Object()) {
            x++;
        }
    }
}

运行测试并立即启用 -prof perfnorm 分析器,结果如下:

Benchmark                                   Mode  Cnt   Score    Error  Units
LockElision.baseline                        avgt   15   0.268 ±  0.001  ns/op
LockElision.baseline:CPI                    avgt    3   0.200 ±  0.009   #/op
LockElision.baseline:L1-dcache-loads        avgt    3   2.035 ±  0.101   #/op
LockElision.baseline:L1-dcache-stores       avgt    310⁻³            #/op
LockElision.baseline:branches               avgt    3   1.016 ±  0.046   #/op
LockElision.baseline:cycles                 avgt    3   1.017 ±  0.024   #/op
LockElision.baseline:instructions           avgt    3   5.076 ±  0.346   #/op
LockElision.locked                          avgt   15   0.268 ±  0.001  ns/op
LockElision.locked:CPI                      avgt    3   0.200 ±  0.005   #/op
LockElision.locked:L1-dcache-loads          avgt    3   2.024 ±  0.237   #/op
LockElision.locked:L1-dcache-stores         avgt    310⁻³            #/op
LockElision.locked:branches                 avgt    3   1.014 ±  0.047   #/op
LockElision.locked:cycles                   avgt    3   1.015 ±  0.012   #/op
LockElision.locked:instructions             avgt    3   5.062 ±  0.154   #/op

测试结果完全相同:执行时间相同,load、store、执行周期和指令数量也一样。这意味着很可能生成的代码也一样,事实也的确如此。生成代码如下:

14.50%   16.97%  ↗  incl   0xc(%r8)              ; 字段值增加
76.82%   76.05%  │  movzbl 0x94(%r9),%r10d       ; JMH 基础框架: 执行下一次 @Benchmark
 0.83%    0.10%  │  add    $0x1,%rbp
 0.47%    0.78%  │  test   %eax,0x15ec6bba(%rip)
 0.47%    0.36%  │  test   %r10d,%r10d
                 ╰  je     BACK

可以看到,锁在这里被完全省略了,没有分配、没有同步。如果配合 -XX:-EliminateLocks 运行,或者用 -XX:-DoEscapeAnalysis 禁用逃逸分析(使用该参数会破坏依赖逃逸分析的所有优化,包括锁省略)。这时锁开销会迅速增加:

Benchmark                                   Mode  Cnt   Score    Error  Units
LockElision.baseline                        avgt   15   0.268 ±  0.001  ns/op
LockElision.baseline:CPI                    avgt    3   0.200 ±  0.001   #/op
LockElision.baseline:L1-dcache-loads        avgt    3   2.029 ±  0.082   #/op
LockElision.baseline:L1-dcache-stores       avgt    3   0.001 ±  0.001   #/op
LockElision.baseline:branches               avgt    3   1.016 ±  0.028   #/op
LockElision.baseline:cycles                 avgt    3   1.015 ±  0.014   #/op
LockElision.baseline:instructions           avgt    3   5.078 ±  0.097   #/op
LockElision.locked                          avgt   15  11.590 ±  0.009  ns/op
LockElision.locked:CPI                      avgt    3   0.998 ±  0.208   #/op
LockElision.locked:L1-dcache-loads          avgt    3  11.872 ±  0.686   #/op
LockElision.locked:L1-dcache-stores         avgt    3   5.024 ±  1.019   #/op
LockElision.locked:branches                 avgt    3   9.027 ±  1.840   #/op
LockElision.locked:cycles                   avgt    3  44.236 ±  3.364   #/op
LockElision.locked:instructions             avgt    3  44.307 ±  9.954   #/op

上面还展示了内存分配以及同步相关开销。

5. 观察

锁省略是开启逃逸分析后带来的一种优化,去除了一些多余的同步。在 synchronized 实现中如果没有发生逃逸尤其利于优化:可以完全移除同步。这是一种编译器优化技术——如果没有看见,那么还需要同步锁吗?

好文章,我在看❤️

原文始发于微信公众号(ImportNew):JVM 解剖公园(19): 锁省略