JVM锁粗化和循环

“ 本文是按照自己的理解翻译自JVM Anatomy Quark #1: Lock Coarsening and Loops,点击文末阅读原文查看原文。”

01—问题

众所周知,在 Hotspot 虚拟机中会对加锁变量进行逃逸分析,并对锁进行粗化。它会将相邻的锁区块进行合并,来降低锁的开销。

synchronized (obj) {
  // statements 1
}
synchronized (obj) {
  // statements 2
}

优化后:

synchronized (obj) {
  // statements 1
  // statements 2
}

译者注:逃逸分析(Escape analysis)可以分析出当前变量是否有可能被其他线程使用,如果只被当前线程使用就是线程安全的就可以消除锁。

那现在个问题,对于循环来说 Hotspot 虚拟机会做优化吗?

for (...) {
  synchronized (obj) {
    // something
  }
}

会被优化成如下代码吗?

synchronized (this) {
  for (...) {
     // something
  }
}

理论上是可以做出优化,这里有点像循环判断外提(loop unswitching)。但是实际上并不会这样做,想一想如果提到外面,这个 for 循环会耗时很久,那将造成一直持有锁。

# 译者注:循环判断外提 是一种编译器优化手段
int i, w, x[1000], y[1000];
for (i = 0; i < 1000; i++) {
    x[i] = x[i] + y[i];
    if (w)
      y[i] = 0;
}
# 优化后
int i, w, x[1000], y[1000];
if (w) {
  for (i = 0; i < 1000; i++) {
    x[i] = x[i] + y[i];
    y[i] = 0;
  }
} else {
  for (i = 0; i < 1000; i++) {
    x[i] = x[i] + y[i];
  }
}

02—实验验证

JMH 不仅是强大的基准测试工具,我们也可以用它进行分析程序。

@Fork(..., jvmArgsPrepend = {"-XX:-UseBiasedLocking"})
@State(Scope.Benchmark)
public class LockRoach {
    int x;

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

这里有几个小技巧:

在配置为 i7 4790K, Linux x86_64, JDK EA 9b156 的机器上:

Benchmark            Mode  Cnt      Score    Error  Units
LockRoach.test       avgt    5   5331.617 ± 19.051  ns/op

从上面运行结果中我们无法得出任何结论,我们需要知道程序运行中究竟发生了什么。

-prof perfasm 通过这个命令可以打印出代码运行时热点区域的汇编代码。加上这个参数运行,我们发现代码中执行最多的指令是 lock cmpxchg (compare and sets),另外只打印了该指令附近的操作。通过 -prof perfasm:mergeMargin=1000 可以放宽打印范围,输出更多内容。

译者注:mergeMargin 默认值 32参数使用方式:java -jar jmh-test.jar jvm.anatomy.LockRoach -prof perfasm:mergeMargin=1000

通过分析输出的汇编代码,我们发现关联最多的是 locking/unlocking 操作,并且可以注意到如下执行次数比较多的片段:

 ↗  0x00007f455cc708c1: lea    0x20(%rsp),%rbx
 │          < blah-blah-blah, monitor enter >     ; <--- coarsened!
 │  0x00007f455cc70918: mov    (%rsp),%r10        ; load $this
 │  0x00007f455cc7091c: mov    0xc(%r10),%r11d    ; load $this.x
 │  0x00007f455cc70920: mov    %r11d,%r10d        ; ...hm...
 │  0x00007f455cc70923: add    $0x42,%r10d        ; ...hmmm...
 │  0x00007f455cc70927: mov    (%rsp),%r8         ; ...hmmmmm!...
 │  0x00007f455cc7092b: mov    %r10d,0xc(%r8)     ; LOL Hotspot, redundant store, killed two lines below
 │  0x00007f455cc7092f: add    $0x108,%r11d       ; add 0x108 = 0x42 * 4 <-- unrolled by 4
 │  0x00007f455cc70936: mov    %r11d,0xc(%r8)     ; store $this.x back
 │          < blah-blah-blah, monitor exit >      ; <--- coarsened!
 │  0x00007f455cc709c6: add    $0x4,%ebp          ; c += 4   <--- unrolled by 4
 │  0x00007f455cc709c9: cmp    $0x3e5,%ebp        ; c < 1000?
 ╰  0x00007f455cc709cf: jl     0x00007f455cc708c1

这里循环展开(loop unrolling)了,展开的粒度是 4。可以通过 -XX:LoopUnrollLimit=1 来关闭编译器的循环展开优化。

Benchmark            Mode  Cnt      Score    Error  Units
# Default
LockRoach.test       avgt    5   5331.617 ± 19.051  ns/op

# -XX:LoopUnrollLimit=1
LockRoach.test       avgt    5  20679.043 ±  3.133  ns/op

4倍的性能差别,这是显而易见的,我们代码执行的热点片段是 lock cmpxchg 即程序加锁的部分,编译器通过循环优化将加锁操作减少了 4 倍,对应程序的吞吐量就增加了 4 倍。

# 循环展开(loop unrolling)最常用来降低循环开销,为具有多个功能单元的处理器提供指令级并行。
# 也有利于指令流水线的调度。
for (i = 1; i <= 60; i++) 
   a[i] = a[i] * b + c;
# 可以展开为:
for (i = 1; i <= 58; i+=3){
  a[i] = a[i] * b + c;
  a[i+1] = a[i+1] * b + c;
  a[i+2] = a[i+2] * b + c;
}

我们可以得出结论了吗?还不行,我们还要再看下关闭 loop unrolling 后程序还是像上面那样每次循环加锁,只不过是每次只走一步。

↗  0x00007f964d0893d2: lea    0x20(%rsp),%rbx
 │          < blah-blah-blah, monitor enter >
 │  0x00007f964d089429: mov    (%rsp),%r10        ; load $this
 │  0x00007f964d08942d: addl   $0x42,0xc(%r10)    ; $this.x += 0x42
 │          < blah-blah-blah, monitor exit >
 │  0x00007f964d0894be: inc    %ebp               ; c++
 │  0x00007f964d0894c0: cmp    $0x3e8,%ebp        ; c < 1000?
 ╰  0x00007f964d0894c6: jl     0x00007f964d0893d2 ;

03—结论

虽然 Hotspot 不会对整个循环进行锁粗化,但是另一个编译器的优化-循环展开,为锁粗化做了准备。循环展开相当于对 N 个连续的加锁的代码块合并成了一个,这样既限制的粗化的粒度,又避免了过度粗化。

微信搜索 CoderMeng 关注获取更多干货文章。
在这里插入图片描述

大家觉得写的不错欢迎点赞评论分享支持。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值