Fra*_*ani 0 optimization performance jvm microbenchmark jmh
我多次注意到,微小的、看似无关的代码更改可以改变一段 Java 代码的性能特征,有时甚至是戏剧性的。
这发生在 JMH 和手动基准测试中。
例如,在这样的类中:
class Class<T> implements Interface {
private final Type field;
Class(ClassBuilder builder) {
field = builder.getField();
}
@Override
void method() { /* ... */ }
}
Run Code Online (Sandbox Code Playgroud)
我做了这个代码更改:
class Class<T> implements Interface {
private static Class<?> instance;
private final Type field;
Class(Builder builder) {
instance = this;
field = builder.getField();
}
@Override
void method() { /* ... */ }
}
Run Code Online (Sandbox Code Playgroud)
和性能发生了巨大变化。
这只是一个例子。在其他情况下,我注意到了同样的事情。
我无法确定是什么原因造成的。我在网上搜索,但没有找到任何信息。
对我来说,它看起来完全无法控制。也许这与编译后的代码在内存中的布局有关?
我不认为这是由于错误共享(见下文)。
我正在开发一个自旋锁:
class SpinLock {
@Contended // Add compiler option: --add-exports java.base/jdk.internal.vm.annotation=<module-name> (if project is not modular, <module-name> is 'ALL-UNNAMED')
private final AtomicBoolean state = new AtomicBoolean();
void lock() {
while (state.getAcquireAndSetPlain(true)) {
while (state.getPlain()) { // With x86 PAUSE we avoid opaque load
Thread.onSpinWait();
}
}
}
void unlock() {
state.setRelease(false);
}
}
class AtomicBoolean {
private static final VarHandle VALUE;
static {
try {
VALUE = MethodHandles.lookup().findVarHandle(AtomicBoolean.class, "value", boolean.class);
} catch (ReflectiveOperationException e) {
throw new ExceptionInInitializerError(e);
}
}
private boolean value;
public boolean getPlain() {
return value;
}
public boolean getAcquireAndSetPlain(boolean value) {
return (boolean) VALUE.getAndSetAcquire(this, value);
}
public void setRelease(boolean value) {
VALUE.setRelease(this, value);
}
}
Run Code Online (Sandbox Code Playgroud)
我的手卷基准报告171.26ns ± 43%和 JMH 基准报告avgt 5 265.970 ± 27.712 ns/op。
当我像这样改变它时:
class SpinLock {
@Contended
private final AtomicBoolean state = new AtomicBoolean();
private final NoopBusyWaitStrategy busyWaitStrategy;
SpinLock() {
this(new NoopBusyWaitStrategy());
}
SpinLock(NoopBusyWaitStrategy busyWaitStrategy) {
this.busyWaitStrategy = busyWaitStrategy;
}
void lock() {
while (state.getAcquireAndSetPlain(true)) {
busyWaitStrategy.reset(); // Will be inlined
while (state.getPlain()) {
Thread.onSpinWait();
busyWaitStrategy.tick(); // Will be inlined
}
}
}
void unlock() {
state.setRelease(false);
}
}
class NoopBusyWaitStrategy {
void reset() {}
void tick() {}
}
Run Code Online (Sandbox Code Playgroud)
我的手卷基准报告184.24ns ± 48%和 JMH 基准报告avgt 5 291.285 ± 20.860 ns/op。尽管两个基准的结果不同,但它们都在增加。
这是 JMH 基准:
public class SpinLockBenchmark {
@State(Scope.Benchmark)
public static class BenchmarkState {
final SpinLock lock = new SpinLock();
}
@Benchmark
@Fork(value = 1, warmups = 1, jvmArgsAppend = {"-Xms8g", "-Xmx8g", "-XX:+AlwaysPreTouch", "-XX:+UnlockExperimentalVMOptions", "-XX:+UseEpsilonGC", "-XX:-RestrictContended"})
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@BenchmarkMode(Mode.AverageTime)
@Threads(6)
public void run(BenchmarkState state) {
state.lock.lock();
state.lock.unlock();
}
}
Run Code Online (Sandbox Code Playgroud)
你有什么想法?
没有运行时的语言也会发生这种情况吗?
您的“微不足道”更改看起来并不那么微不足道。
你添加了 busyWaitStrategy.tick()在热循环中call,这会导致额外的加载、比较和 [non-taken] 条件分支。
即使该方法什么都不做,JLS 也需要NullPointerException在null对象上调用方法时抛出。因此,JVM 需要加载该字段并检查它是否为空。尽管该字段已声明final,但 HotSpot JVM并未将其视为常量。并且由于Thread.onSpinWait,现场负载不会被提升到循环之外,因为它用作membar(参见讨论线程)。
借助-XX:+PrintAssembly,我们确实可以在编译后的代码中看到这个空指针检查:
pause ;*invokestatic onSpinWait {reexecute=0 rethrow=0 return_oop=0}
; - bench.SpinLock::lock@28 (line 23)
; - bench.SpinLockBenchmark::run@4 (line 17)
; - bench.generated.SpinLockBenchmark_run_jmhTest::run_avgt_jmhStub@17
>> cmp r12d,dword ptr [r10+10h]
>> je 1a01286f719h ;*invokevirtual tick {reexecute=0 rethrow=0 return_oop=0}
; - bench.SpinLock::lock@35 (line 24)
; - bench.SpinLockBenchmark::run@4 (line 17)
; - bench.generated.SpinLockBenchmark_run_jmhTest::run_avgt_jmhStub@17
mov r8d,dword ptr [r10+0ch] ;*getfield state {reexecute=0 rethrow=0 return_oop=0}
; - bench.SpinLock::lock@19 (line 22)
; - bench.SpinLockBenchmark::run@4 (line 17)
; - bench.generated.SpinLockBenchmark_run_jmhTest::run_avgt_jmhStub@17
Run Code Online (Sandbox Code Playgroud)
此外,@Contended注释似乎被滥用了。据我了解代码,目的是保护AtomicBoolean 对象免受错误共享,而不是引用。因此,将AtomicBoolean.value字段或整个AtomicBoolean类标记为更有意义@Contended.
对于微基准测试结果的调查,我建议使用 JMH 内置分析器:-prof perfasm和-prof perfnorm(顺便说一句,这是 JMH 超过手动框架的另一个原因)。perfasm将显示汇编代码 - 占用最多 cpu 周期的特定指令。perfnorm将输出性能计数器统计信息,例如每个周期的指令、缓存未命中、错误预测的分支等。
| 归档时间: |
|
| 查看次数: |
161 次 |
| 最近记录: |