“ 本文是按照自己的理解翻译自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
关注获取更多干货文章。
大家觉得写的不错欢迎点赞
、评论
、分享
支持。