意外的 VarHandle 性能(比替代方案慢 4 倍)
Unexpected VarHandle performance (4X slower than alternatives)
在 JEP193 中,VarHandles
的具体目标之一是提供使用 FieldUpdaters
和 AtomicIntegers
的替代方法(并避免一些相关的开销跟他们)。
AtomicIntegers
在内存方面可能特别浪费,因为它们是一个单独的对象(它们每个使用大约 36 个字节,具体取决于几个因素,例如是否启用压缩 OOP 等。 ).
如果您有许多可能需要原子更新的整数(在许多小对象中),如果您想减少浪费,基本上有三个选项:
- 使用
AtomicFieldUpdater
- 使用
VarHandle
- 或重新编写运行代码以使用
AtomicIntegerArray
而不是对象中的字段。
所以我决定测试备选方案并了解每个备选方案的性能影响。
使用整数字段的原子(易失性模式)增量作为代理,我在 2014 年中的 MacBook Pro 上得到以下结果:
Benchmark Mode Cnt Score Error Units
VarHandleBenchmark.atomic thrpt 5 448041037.223 ± 36448840.301 ops/s
VarHandleBenchmark.atomicArray thrpt 5 453785339.203 ± 64528885.282 ops/s
VarHandleBenchmark.fieldUpdater thrpt 5 459802512.169 ± 52293792.737 ops/s
VarHandleBenchmark.varhandle thrpt 5 136482396.440 ± 9439041.030 ops/s
在此基准测试中,VarHandles
大约 慢四倍。
我想了解的是开销从何而来?
这是签名多态访问方式的问题吗?我在微基准测试中犯了错误吗?
基准详情如下。
我 运行 在 2014 年中期 MacBook Pro 上使用以下 JVM 进行基准测试
> java -version
openjdk version "11.0.2" 2019-01-15
OpenJDK Runtime Environment AdoptOpenJDK (build 11.0.2+9)
OpenJDK 64-Bit Server VM AdoptOpenJDK (build 11.0.2+9, mixed mode)
基准测试源代码:
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.Fork;
import org.openjdk.jmh.annotations.Measurement;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.annotations.Threads;
import org.openjdk.jmh.annotations.Warmup;
import org.openjdk.jmh.infra.Blackhole;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.VarHandle;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicIntegerArray;
import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
@State(Scope.Thread)
@Fork(value = 1, jvmArgs = {"-Xms256m", "-Xmx256m", "-XX:+UseG1GC"})
@Warmup(iterations = 3, time = 3)
@Measurement(iterations = 5, time = 5)
@Threads(4)
public class VarHandleBenchmark {
// array option
private final AtomicIntegerArray array = new AtomicIntegerArray(1);
// vanilla AtomicInteger
private final AtomicInteger counter = new AtomicInteger();
// count field and its VarHandle
private volatile int count;
private static final VarHandle COUNT;
// count2 field and its field updater
private volatile int count2;
private static final AtomicIntegerFieldUpdater<VarHandleBenchmark> COUNT2 ;
static {
try {
COUNT = MethodHandles.lookup()
.findVarHandle(VarHandleBenchmark.class, "count", Integer.TYPE);
COUNT2 = AtomicIntegerFieldUpdater.newUpdater(VarHandleBenchmark.class, "count2");
} catch (ReflectiveOperationException e) {
throw new AssertionError(e);
}
}
@Benchmark
public void atomic(Blackhole bh) {
bh.consume(counter.getAndAdd(1));
}
@Benchmark
public void atomicArray(Blackhole bh) {
bh.consume(array.getAndAdd(0, 1));
}
@Benchmark
public void varhandle(Blackhole bh) {
bh.consume(COUNT.getAndAdd(this, 1));
}
@Benchmark
public void fieldUpdater(Blackhole bh) {
bh.consume(COUNT2.getAndAdd(this, 1));
}
}
更新:应用apangin的解决方案后,这些是基准测试的结果:
Benchmark Mode Cnt Score Error Units
VarHandleBenchmark.atomic thrpt 5 464045527.470 ± 42337922.645 ops/s
VarHandleBenchmark.atomicArray thrpt 5 465700610.882 ± 18116770.557 ops/s
VarHandleBenchmark.fieldUpdater thrpt 5 473968453.591 ± 49859839.498 ops/s
VarHandleBenchmark.varhandle thrpt 5 429737922.796 ± 41629104.677 ops/s
差异消失。
VarHandle.getAndAdd
是一种 signature polymorphic 方法。也就是说,它的参数类型和它的 return 值的类型是从实际的源代码中派生出来的。
Blackhole.consume
是一个重载方法。此方法有多种变体:
- 消耗(整数)
- 消费(对象)
- 等等
在您的代码中,根据语言规则,使用了consume(Object)
方法。因此,VarHandle 也 return 是一个对象——一个装箱的整数。
为了使用正确的方法,您需要重写 varhandle
基准测试,如下所示:
bh.consume((int) COUNT.getAndAdd(this, 1));
现在 varhandle
将 运行 具有与其他基准相同的性能。
在 JEP193 中,VarHandles
的具体目标之一是提供使用 FieldUpdaters
和 AtomicIntegers
的替代方法(并避免一些相关的开销跟他们)。
AtomicIntegers
在内存方面可能特别浪费,因为它们是一个单独的对象(它们每个使用大约 36 个字节,具体取决于几个因素,例如是否启用压缩 OOP 等。 ).
如果您有许多可能需要原子更新的整数(在许多小对象中),如果您想减少浪费,基本上有三个选项:
- 使用
AtomicFieldUpdater
- 使用
VarHandle
- 或重新编写运行代码以使用
AtomicIntegerArray
而不是对象中的字段。
所以我决定测试备选方案并了解每个备选方案的性能影响。
使用整数字段的原子(易失性模式)增量作为代理,我在 2014 年中的 MacBook Pro 上得到以下结果:
Benchmark Mode Cnt Score Error Units
VarHandleBenchmark.atomic thrpt 5 448041037.223 ± 36448840.301 ops/s
VarHandleBenchmark.atomicArray thrpt 5 453785339.203 ± 64528885.282 ops/s
VarHandleBenchmark.fieldUpdater thrpt 5 459802512.169 ± 52293792.737 ops/s
VarHandleBenchmark.varhandle thrpt 5 136482396.440 ± 9439041.030 ops/s
在此基准测试中,VarHandles
大约 慢四倍。
我想了解的是开销从何而来?
这是签名多态访问方式的问题吗?我在微基准测试中犯了错误吗?
基准详情如下。
我 运行 在 2014 年中期 MacBook Pro 上使用以下 JVM 进行基准测试
> java -version
openjdk version "11.0.2" 2019-01-15
OpenJDK Runtime Environment AdoptOpenJDK (build 11.0.2+9)
OpenJDK 64-Bit Server VM AdoptOpenJDK (build 11.0.2+9, mixed mode)
基准测试源代码:
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.Fork;
import org.openjdk.jmh.annotations.Measurement;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.annotations.Threads;
import org.openjdk.jmh.annotations.Warmup;
import org.openjdk.jmh.infra.Blackhole;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.VarHandle;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicIntegerArray;
import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
@State(Scope.Thread)
@Fork(value = 1, jvmArgs = {"-Xms256m", "-Xmx256m", "-XX:+UseG1GC"})
@Warmup(iterations = 3, time = 3)
@Measurement(iterations = 5, time = 5)
@Threads(4)
public class VarHandleBenchmark {
// array option
private final AtomicIntegerArray array = new AtomicIntegerArray(1);
// vanilla AtomicInteger
private final AtomicInteger counter = new AtomicInteger();
// count field and its VarHandle
private volatile int count;
private static final VarHandle COUNT;
// count2 field and its field updater
private volatile int count2;
private static final AtomicIntegerFieldUpdater<VarHandleBenchmark> COUNT2 ;
static {
try {
COUNT = MethodHandles.lookup()
.findVarHandle(VarHandleBenchmark.class, "count", Integer.TYPE);
COUNT2 = AtomicIntegerFieldUpdater.newUpdater(VarHandleBenchmark.class, "count2");
} catch (ReflectiveOperationException e) {
throw new AssertionError(e);
}
}
@Benchmark
public void atomic(Blackhole bh) {
bh.consume(counter.getAndAdd(1));
}
@Benchmark
public void atomicArray(Blackhole bh) {
bh.consume(array.getAndAdd(0, 1));
}
@Benchmark
public void varhandle(Blackhole bh) {
bh.consume(COUNT.getAndAdd(this, 1));
}
@Benchmark
public void fieldUpdater(Blackhole bh) {
bh.consume(COUNT2.getAndAdd(this, 1));
}
}
更新:应用apangin的解决方案后,这些是基准测试的结果:
Benchmark Mode Cnt Score Error Units
VarHandleBenchmark.atomic thrpt 5 464045527.470 ± 42337922.645 ops/s
VarHandleBenchmark.atomicArray thrpt 5 465700610.882 ± 18116770.557 ops/s
VarHandleBenchmark.fieldUpdater thrpt 5 473968453.591 ± 49859839.498 ops/s
VarHandleBenchmark.varhandle thrpt 5 429737922.796 ± 41629104.677 ops/s
差异消失。
VarHandle.getAndAdd
是一种 signature polymorphic 方法。也就是说,它的参数类型和它的 return 值的类型是从实际的源代码中派生出来的。
Blackhole.consume
是一个重载方法。此方法有多种变体:
- 消耗(整数)
- 消费(对象)
- 等等
在您的代码中,根据语言规则,使用了consume(Object)
方法。因此,VarHandle 也 return 是一个对象——一个装箱的整数。
为了使用正确的方法,您需要重写 varhandle
基准测试,如下所示:
bh.consume((int) COUNT.getAndAdd(this, 1));
现在 varhandle
将 运行 具有与其他基准相同的性能。