【问题标题】:Unexpected VarHandle performance (4X slower than alternatives)出乎意料的 VarHandle 性能(比替代方案慢 4 倍)
【发布时间】:2020-03-10 18:52:41
【问题描述】:

JEP193 中,VarHandles 的具体目标之一是提供使用FieldUpdatersAtomicIntegers 的替代方案(并避免与它们相关的一些开销)。

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

差异消失了。

【问题讨论】:

    标签: java multithreading concurrency jvm jvm-hotspot


    【解决方案1】:

    VarHandle.getAndAdd 是一个signature polymorphic 方法。即其参数的类型和返回值的类型均来源于实际源码。

    Blackhole.consume 是一个重载方法。这种方法有多种变体:

    • 消费(int)
    • 消费(对象)

    在您的代码中,根据语言规则,使用consume(Object) 方法。因此,VarHandle 也返回一个 Object - 一个装箱的 Integer。

    为了使用正确的方法,你需要重写varhandle benchmark如下:

    bh.consume((int) COUNT.getAndAdd(this, 1));
    

    现在varhandle 将以与其他基准测试相同的性能运行。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2021-04-12
      • 2014-07-01
      • 1970-01-01
      • 2011-05-24
      • 1970-01-01
      • 2012-10-14
      • 1970-01-01
      相关资源
      最近更新 更多