java 锁粗化_JVM锁粗化和循环

众所周知,JVM可以进行锁粗化优化,可以有效地合并多个相邻的加锁代码块,因此可以减少加锁的成本。该特性可以将下述代码:

synchronized (obj) {

// statements 1

}

synchronized (obj) {

// statements 2

}

变为:

synchronized (obj) {

// statements 1

// statements 2

}

那么Hotspot 对循环也会做这种优化么?比如:

for (...) {

synchronized (obj) {

// something

}

}

是否会优化成这样:

synchronized (this) {

for (...) {

// something

}

}

理论上来说是可以的,这有点像针对锁的循环判断外提。然而如此优化的缺点是将锁的粒度增加太多,线程在执行循环时将会长时间独占锁。

实验

我们从一个简单的例子开始:

@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;

}

}

}

}

这里有几个重要的技巧:通过-XX:-UseBiasedLocking关闭偏向锁,以避免长时间的预热,因为偏向锁并不是直接启动,而是完成初始化阶段后等待5秒钟才开始。(参看BiasedLockingStartupDelay选项)(译者注:如果确定应用的锁通常情况下处于竞争状态,可以通过JVM参数关闭偏向锁:-XX:-UseBiasedLocking=false,那么程序默认会进入轻量级锁状态。)

关闭@Benchmark方法的内联可以帮忙我们从反汇编中分离出相关代码。

每次累加一个魔数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

我们需要深入看JVM做了什么操作。-prof perfasm这个参数非常有用,它可以展示生成代码中最热的部分。使用默认参数执行代码,发现代码中最热的指令是执行锁操作的lock cmpxchg(compare-and-sets),另外也输出了该指令附近的操作。通过设置-prof perfasm:mergeMargin=1000

来放宽输出热区代码的范围。

进一步剥离这些代码,同时注意累积最多周期的代码(第一列),我们可以看到最热的循环是:

↗ 0x00007f455cc708c1: lea 0x20(%rsp),%rbx

│ < blah-blah-blah, monitor enter > ;

│ 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

│ 0x00007f455cc70936: mov %r11d,0xc(%r8) ; store $this.x back

│ < blah-blah-blah, monitor exit > ;

│ 0x00007f455cc709c6: add $0x4,%ebp ; c += 4

│ 0x00007f455cc709c9: cmp $0x3e5,%ebp ; c < 1000?

╰ 0x00007f455cc709cf: jl 0x00007f455cc708c1

循环以4为步长展开了,然后锁通过这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

因为我们已经观察到最热的指令是锁操作lock cmpxchg,所以可以预期有4倍的性能提升。4倍的锁粗化意味着4倍的吞吐量。继续验证关闭循环展开后的反汇编代码。通过 perfasm 可以看出代码在做类似的循环,只是每次只走一步。

↗ 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 ;

看起来一切正常。

结论

虽然 Hotspot 不会对整个循环进行锁粗化,但是使用循环展开优化为锁粗化做了准备,因为循环展开后的代码就是 N 个连续的加锁代码块。这样既获得了性能收益,又限制了粗化的粒度,避免了对循环的过度粗化。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值