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

ImportNew ImportNew 2019-08-29

(给ImportNew加星标,提高Java技能)

编译: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 实现中如果没有发生逃逸尤其利于优化:可以完全移除同步。这是一种编译器优化技术——如果没有看见,那么还需要同步锁吗?


推荐阅读

(点击标题可跳转阅读)

JVM 解剖公园(18): 标量替换

JVM 解剖公园(17): 信任非 static final 字段

JVM 解剖公园(16): 超多态虚拟调用


看完本文有收获?请转发分享给更多人

关注「ImportNew」,提升Java技能

好文章,我在看❤️

    已同步到看一看

    发送中

    本站仅按申请收录文章,版权归原作者所有
    如若侵权,请联系本站删除
    觉得不错,分享给更多人看到