使用自己的int容量比使用数组的.length字段更快?

时间:2015-05-31 23:36:32

标签: java performance caching concurrency

"95% of performance is about clean representative models"Martin Thompson讲话中,在17到21分钟之间,会出现以下代码:

public class Queue
{
    private final Object[] buffer;
    private final int capacity;

    // Rest of the code

}

在20:16他说:

  

你可以获得更好的表现,所以留下像capacity这样的东西   在这是正确的事情。

我尝试提出一个代码示例,其中capacity将比buffer.length快得多,但我失败了。

马丁说两个场景出现了问题:

  1. 在并发的世界中。但是,length字段也是finalJLS 10.7。所以,我不知道这可能是一个什么问题。
  2. 当缓存未命中时。tried调用capacity vs buffer.length一百万次(队列中有一百万个元素),但没有显着差异。我使用JMH进行基准测试。
  3. 您能否提供一个代码示例,该示例演示了capacity在性能方面优于buffer.length的情况?

    更常见的情况(经常在实际代码中发现)越多越好。

    请注意,我完全取消了美学,清洁代码,代码重新分解等方面的内容。我只询问性能。

4 个答案:

答案 0 :(得分:9)

当您正常访问数组时,JVM仍然使用其length来执行边界检查。但是当你通过sun.misc.Unsafe(像马丁那样)访问数组时,你不必支付这种隐含的惩罚。

数组的length字段通常与第一个元素位于同一个缓存行中,因此当多个线程同时写入第一个索引时,您将拥有false sharing。使用单独的字段来缓冲容量将打破这种错误共享。

这是一个基准测试,显示capacity字段如何使数组访问速度更快:

package bench;

import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.Param;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.Setup;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.annotations.Threads;
import sun.misc.Unsafe;

import java.lang.reflect.Field;
import java.util.concurrent.atomic.AtomicReferenceArray;

@State(Scope.Benchmark)
@Threads(4)
public class Queue {
    private static final Unsafe unsafe = getUnsafe();
    private static final long base = unsafe.arrayBaseOffset(Object[].class);
    private static final int scale = unsafe.arrayIndexScale(Object[].class);

    private AtomicReferenceArray<Object> atomic;
    private Object[] buffer;
    private int capacity;

    @Param({"0", "25"})
    private volatile int index;

    @Setup
    public void setup() {
        capacity = 32;
        buffer = new Object[capacity];
        atomic = new AtomicReferenceArray<>(capacity);
    }

    @Benchmark
    public void atomicArray() {
        atomic.set(index, "payload");
    }

    @Benchmark
    public void unsafeArrayLength() {
        int index = this.index;
        if (index < 0 || index >= buffer.length) {
            throw new ArrayIndexOutOfBoundsException();
        }
        unsafe.putObjectVolatile(buffer, base + index * scale, "payload");
    }

    @Benchmark
    public void unsafeCapacityField() {
        int index = this.index;
        if (index < 0 || index >= capacity) {
            throw new ArrayIndexOutOfBoundsException();
        }
        unsafe.putObjectVolatile(buffer, base + index * scale, "payload");
    }

    private static Unsafe getUnsafe() {
        try {
            Field f = Unsafe.class.getDeclaredField("theUnsafe");
            f.setAccessible(true);
            return (Unsafe) f.get(null);
        } catch (IllegalAccessException | NoSuchFieldException e) {
            throw new AssertionError("Should not happen");
        }
    }
}

结果:

Benchmark                  (index)   Mode  Cnt      Score      Error   Units
Queue.atomicArray                0  thrpt    5  41804,825 ±  928,882  ops/ms
Queue.atomicArray               25  thrpt    5  84713,201 ± 1067,911  ops/ms
Queue.unsafeArrayLength          0  thrpt    5  48656,296 ±  676,166  ops/ms
Queue.unsafeArrayLength         25  thrpt    5  88812,863 ± 1089,380  ops/ms
Queue.unsafeCapacityField        0  thrpt    5  88904,433 ±  360,936  ops/ms
Queue.unsafeCapacityField       25  thrpt    5  88633,490 ± 1426,329  ops/ms

答案 1 :(得分:3)

你不应该直接克服马丁的话。当他说'#34;使用array.length是一种反模式,复制在项目上时,我认为这是狡猾。

使用capacity字段确实可以改善局部性,减少污染缓存并有助于避免错误共享,但它需要编写非常可怕的源代码,这远远不是&#34;干净简单& #34;,马丁在这次演讲中做广告。

问题是,即使您不直接在源代码中编写array.length,JVM仍然可以在每个索引array[i]的数组上访问长度(即访问数组头),检查边界。 Hotspot JVM has issues with eliminating bounds checks even in "simple" looping cases,我认为它无法解释一些&#34;外部&#34;像if (i < capacity) return array[i];那样检查作为绑定检查,i。即绑定容量字段和数组大小。

这就是为什么要让capacity - 模式有意义,你只需要通过Unsafe访问数组! That, unfortunately, disables many bulk loop optimizations.

Look at Martin's "clean" queue implementation :)

我也可以尝试解释在访问&#34; final&#34;时同时考虑因素的含义。 array.length。我的实验表明,甚至&#34;读 - 读&#34;并发缓存行访问引入了某种“错误共享”#34;并减慢了事情。 (我认为JVM工程师在使@sun.misc.Contended从竞争字段的两个侧偏移128个字节时考虑了这一点;这可能是为了确保双边缓存行预取和&#34;读 - 读错误共享&#34;不会影响性能。)

这就是为什么当队列使用者和生产者访问容纳环绕缓冲区的容量时,他们更好地访问不同的对象,包含相同的(按值) {{ 1}}字段,以及对相同数组的引用。通过不安全的生产者和计算机访问此阵列通常访问该阵列的不同区域,不要错误地共享任何内容。

IMO反模式现在是尝试实现另一个capacity,而https://github.com/JCTools/JCTools后面的人(包括Martin,顺便说一句)将其优化为死亡。

答案 2 :(得分:0)

我不是JVM专家,也不声称理解它的优化。

您是否考虑过查看字节代码以查看执行的指令?

public class Queue {

    private final Object[] buffer;
    private final int capacity;

    public Queue(int size) {
        buffer = new Object[size];
        this.capacity =  size;
    }

    public static void main(String... args) {
        Queue q = new Queue(10);
        int c = q.capacity;
        int l = q.buffer.length;
    }
}

这是上面主要方法的反汇编字节码。

public static void main(java.lang.String...);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC, ACC_VARARGS
    Code:
      stack=3, locals=4, args_size=1
         0: new           #5                  // class Queue
         3: dup
         4: bipush        10
         6: invokespecial #6                  // Method "<init>":(I)V
         9: astore_1

        10: aload_1
        11: getfield      #4                  // Field capacity:I
        14: istore_2

        15: aload_1
        16: getfield      #3                  // Field buffer:[Ljava/lang/Object;
        19: arraylength

        20: istore_3
        21: return

我们看到两者都有 getfield 指令,但是array.length有另外一条指令 arraylength

查看arraylength

的jvm规范
instructionIsTypeSafe(arraylength, Environment, _Offset, StackFrame,
                      NextStackFrame, ExceptionStackFrame) :- 
    nth1OperandStackIs(1, StackFrame, ArrayType),
    arrayComponentType(ArrayType, _),
    validTypeTransition(Environment, [top], int, StackFrame, NextStackFrame),
    exceptionStackFrame(StackFrame, ExceptionStackFrame).

nth1OperandStackIs - 该指令检查传入是否为引用类型并引用数组。如果数组引用为null,则抛出NullPointerException

arrayComponentType - 检查元素的类型。 X数组的组件类型是X

validTypeTransition - 输入检查规则

因此,在数组上调用length有额外的指令arraylength。 非常有兴趣了解这个问题。

答案 3 :(得分:0)

我怀疑这会对性能产生任何积极影响。例如,它无法帮助消除Hotspot中的绑定检查。更糟糕的是:在一个JVM中可能会更快,但可能在下一个版本中会受到伤害。 Java不断获得额外的优化,并且数组边界检查是他们努力优化的一件事......

我相信这可能是重写真实队列代码以创建更简单示例的剩余部分。因为在真实队列中,您需要处理使用的容量,有时您希望允许容量的上限(当消费者无法跟上时阻止生产者)。如果您有这样的代码(具有setCapacity / getCapacity和非最终容量)并通过删除调整大小逻辑和最终确定后备存储来简化它,那么这就是您可能最终得到的结果。