大声思考:值得注意的是,人们经常做 90% 的乏味工作,而把 10%(乐趣开始的地方)留给其他人!好吧,我就尽情享受吧!
让我先在我的 i7-4790K、8u40 EA 上重复一下实验:
Benchmark Mode Samples Score Error Units
UnsafeCounter_Benchmark.atomicCount thrpt 5 47.669 ± 18.440 ops/us
UnsafeCounter_Benchmark.lockCount thrpt 5 14.497 ± 7.815 ops/us
UnsafeCounter_Benchmark.syncNoVCount thrpt 5 11.618 ± 2.130 ops/us
UnsafeCounter_Benchmark.syncVCount thrpt 5 11.337 ± 4.532 ops/us
UnsafeCounter_Benchmark.unsafeCount thrpt 5 7.452 ± 1.042 ops/us
UnsafeCounter_Benchmark.unsafeGACount thrpt 5 43.332 ± 3.435 ops/us
UnsafeCounter_Benchmark.unsyncCount thrpt 5 102.773 ± 11.943 ops/us
确实,有些事情看起来很可疑unsafeCount
测试。实际上,在验证所有数据之前,您必须假设所有数据都是可疑的。对于纳米基准,您必须验证生成的代码,看看您是否真正测量了您想要测量的东西。在 JMH 中,它是很快就能做到 with -prof perfasm
。事实上,如果你看看最热的地区unsafeCount
在那里,你会注意到一些有趣的事情:
0.12% 0.04% 0x00007fb45518e7d1: mov 0x10(%r10),%rax
17.03% 23.44% 0x00007fb45518e7d5: test %eax,0x17318825(%rip)
0.21% 0.07% 0x00007fb45518e7db: mov 0x18(%r10),%r11 ; getfield offset
30.33% 10.77% 0x00007fb45518e7df: mov %rax,%r8
0.00% 0x00007fb45518e7e2: add $0x1,%r8
0.01% 0x00007fb45518e7e6: cmp 0xc(%r10),%r12d ; typecheck
0x00007fb45518e7ea: je 0x00007fb45518e80b ; bail to v-call
0.83% 0.48% 0x00007fb45518e7ec: lock cmpxchg %r8,(%r10,%r11,1)
33.27% 25.52% 0x00007fb45518e7f2: sete %r8b
0.12% 0.01% 0x00007fb45518e7f6: movzbl %r8b,%r8d
0.03% 0.04% 0x00007fb45518e7fa: test %r8d,%r8d
0x00007fb45518e7fd: je 0x00007fb45518e7d1 ; back branch
翻译:a)offset
每次迭代都会重新读取字段——因为 CAS 内存效应意味着易失性读取,因此需要悲观地重新读取该字段; b) 搞笑的部分是unsafe
场是also出于同样的原因,正在重新阅读以进行类型检查。
这就是为什么高性能代码应该如下所示:
--- a/utils bench/src/main/java/org/kirmit/utils/unsafe/concurrency/UnsafeCASCounter.java
+++ b/utils bench/src/main/java/org/kirmit/utils/unsafe/concurrency/UnsafeCASCounter.java
@@ -5,13 +5,13 @@ import sun.misc.Unsafe;
public class UnsafeCASCounter implements Counter {
private volatile long counter = 0;
- private final Unsafe unsafe = UnsafeHelper.unsafe;
- private long offset;
- {
+ private static final Unsafe unsafe = UnsafeHelper.unsafe;
+ private static final long offset;
+ static {
try {
offset = unsafe.objectFieldOffset(UnsafeCASCounter.class.getDeclaredField("counter"));
} catch (NoSuchFieldException e) {
- e.printStackTrace();
+ throw new IllegalStateException("Whoops!");
}
}
如果您这样做,则unsafeCount
性能立即提升:
Benchmark Mode Samples Score Error Units
UnsafeCounter_Benchmark.unsafeCount thrpt 5 9.733 ± 0.673 ops/us
...考虑到误差范围,现在与同步测试相当接近。如果你看一下-prof perfasm
现在,这是一个unsafeCount
loop:
0.08% 0.02% 0x00007f7575191900: mov 0x10(%r10),%rax
28.09% 28.64% 0x00007f7575191904: test %eax,0x161286f6(%rip)
0.23% 0.08% 0x00007f757519190a: mov %rax,%r11
0x00007f757519190d: add $0x1,%r11
0x00007f7575191911: lock cmpxchg %r11,0x10(%r10)
47.27% 23.48% 0x00007f7575191917: sete %r8b
0.10% 0x00007f757519191b: movzbl %r8b,%r8d
0.02% 0x00007f757519191f: test %r8d,%r8d
0x00007f7575191922: je 0x00007f7575191900
这个循环非常紧,似乎没有什么能让它走得更快。我们花费大部分时间加载“更新”的值并实际对其进行 CAS 处理。但我们争论很多!为了弄清楚争用是否是主要原因,让我们添加退避:
--- a/utils bench/src/main/java/org/kirmit/utils/unsafe/concurrency/UnsafeCASCounter.java
+++ b/utils bench/src/main/java/org/kirmit/utils/unsafe/concurrency/UnsafeCASCounter.java
@@ -20,6 +21,7 @@ public class UnsafeCASCounter implements Counter {
long before = counter;
while (!unsafe.compareAndSwapLong(this, offset, before, before + 1L)) {
before = counter;
+ Blackhole.consumeCPU(1000);
}
}
...跑步:
Benchmark Mode Samples Score Error Units
UnsafeCounter_Benchmark.unsafeCount thrpt 5 99.869 ± 107.933 ops/us
瞧。我们的确是more在循环中工作,但它使我们免于很多竞争。我之前尝试解释过这一点“纳米信任纳米时间” http://shipilev.net/blog/2014/nanotrusting-nanotime/,最好返回那里并阅读更多有关基准测试方法的内容,尤其是在测量重量级操作时。这凸显了整个实验中的陷阱,不仅是unsafeCount
.
OP 和感兴趣的读者的练习:解释原因unsafeGACount
and atomicCount
比其他测试执行得快得多。你现在有了工具。
附:在具有 C (C
附言时间检查:10 分钟进行分析和附加实验,20 分钟进行撰写。您浪费了多少时间手动复制结果? ;)