JEP193中,VarHandles的特定目标之一是提供一种替代方法来使用FieldUpdatersAtomicIntegers(并避免与之相关的一些开销)。
AtomicIntegers在内存方面可能特别浪费,因为它们是一个单独的对象(它们每个使用大约36个字节,具体取决于是否启用压缩OOP等一些因素,等等)。

如果您有许多可能需要原子更新的整数(在许多小对象中),则要减少浪费,基本上有三种选择:

  • 使用AtomicFieldUpdater
  • 使用VarHandle
  • 或重新排列代码以使用AtomicIntegerArray而不是对象中的字段。

  • 因此,我决定测试替代方案,并对每种方案的性能含义有所了解。

    使用整数字段的原子( volatile 模式)增量作为代理,我在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.getAndAddsignature polymorphic方法。即,其参数的类型和其返回值的类型是从实际的源代码派生的。
    Blackhole.consume是重载的方法。此方法有多种变体:

  • 消耗(int)
  • 消耗(对象)
  • 等。

  • 在您的代码中,根据语言规则,使用了consume(Object)方法。因此,VarHandle还返回一个对象-装箱的整数。

    为了使用正确的方法,您将需要重写varhandle基准,如下所示:
    bh.consume((int) COUNT.getAndAdd(this, 1));
    

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

    08-28 20:51