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


在里面 “95%的性能是关于干净的代表性模型” 通过谈话 马丁汤普森在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. 当缓存未命中时。 一世 试着 调用 capacity VS buffer.length 一百万次(队列中有一百万个元素),但没有显着差异。我使用JMH进行基准测试。

能否请您提供一个代码示例,其中演示了一个案例 capacity 优于 buffer.length 在表现方面?

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

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


4371
2018-05-31 23:36


起源

我认为容量意味着在重新分配之前可以将多少元素添加到集合中。集合的容量可以为5,但是大小为0,因为其中没有项目。当您向集合中添加项目时,大小将增加,但随后大小超过容量,则将分配更多内存以便能够保存新项目。 - Shar1er80
我会假设 .length 在现代JVM上更快; JIT可能有特殊的方法来优化数组并访问它们 .length 对外场有利。可能特别重要的是JIT在使用数组的紧密循环中将该数组的长度放在寄存器中。您可以尝试使用的基准测试吗? capacity 和 buffer.length 在一个访问数组的紧密循环中? - Andrey Akhmetov
@hexafraction 试着  for (int i = 0; i < queue.getCapacity(); i++) queue.buffer[i] = queue.getCapacity();,吞吐量得分无显着差异。 - Adam Stelmaszczyk
在典型的应用程序中,这是不成熟的过早优化。但我怀疑他在谈论典型的应用程序。紧密循环测试不是具有复杂代码流的实际应用程序的良好表示,需要极高的性能。如果它更具侵略性,JVM甚至可以将你的紧密循环减少到无操作。 - ZhongYu
@ mjpt777你能解释为什么这应该更快吗? - Anony-Mousse


答案:


正常访问数组时,JVM使用它 length 无论如何要执行边界检查。但是当你访问数组时 sun.misc.Unsafe (就像马丁那样),你不必支付这种隐含的惩罚。

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

这是一个显示如何的基准 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

9
2018-06-01 07:03



如果你添加一个普通的数组访问,而不是使用它会很棒 Unsafe作为第三种变体,用于比较。 - Holger
@Holger好点。但是,测量普通数组访问是不公平的,因为它不提供线程间可见性语义。所以我添加了测试用例 AtomicReferenceArray 访问的工作方式几乎相同 unsafeArrayLength 甚至稍微慢一点。 - apangin
为什么索引0这么慢?为什么呢 unsafeArrayLength 比...快 unsafeCapacityField 对于指数25? - Anony-Mousse
@ Anony-Mousse访问索引0的速度较慢,因为它位于旁边 length 数组的字段在缓存行的大小(64字节)内,因此争用了 length 由于错误共享导致的字段:写入索引0不仅使这段内存无效,而且整个缓存行包括 length。 - apangin
至于指数25, unsafeArrayLength 并不快。分数差异基本上在测量精度内。 - apangin


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

使用 capacity field确实允许改善局部性,减少污染缓存并有助于避免错误共享,但它需要编写非常可怕的源代码,这远非“干净和简单”,Martin在本次演讲中做广告。

问题是,即使你不写 array.length 在您的源代码中,JVM无论如何都要访问每个数组索引的长度(即访问数组头) array[i],检查边界。 即使在“简单”循环的情况下,Hotspot JVM也有消除边界检查的问题,我认为它无法解释一些“外部”检查 if (i < capacity) return array[i]; 作为绑定检查,我。即绑定容量字段和数组大小。

这就是为什么要这样做 capacity - 模式有意义,你需要只通过访问数组 Unsafe不幸的是,这会禁用许多批量循环优化。

看看Martin的“干净”队列实现:)

我也可以尝试解释在访问“最终”时在并发考虑因素下的含义 array.length。我的实验表明,即使是“读 - 读”并发缓存行访问也会引入某种“错误共享”并减慢速度。 (我认为JVM工程师在制作时会考虑这个问题 @sun.misc.Contended 偏移128个字节  争夺领域的两面;可能这是为了确保双面缓存行预取和“读 - 读错误共享”不会影响性能。)

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

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


3
2018-06-01 06:31





我不是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

看看jvm规范 arraylength

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。 非常有兴趣了解这个问题的更多信息。


0
2018-06-01 00:20



虽然此链接可能会回答这个问题,但最好在此处包含答案的基本部分并提供参考链接。如果链接页面发生更改,则仅链接答案可能会变为无效。 - Achrome
@Achrome这不是(也从来没有)仅限链接的答案。 - user207421
通过检测字节代码是什么意思?你能提供一个代码示例吗?这是我在问题中唯一要问的问题。我认为你的问题应该是一个评论,这不是一个明确的答案。我看了字节码,但它没有帮我回答我的问题。我们都知道JLS 10.7,不需要链接,因为我已经在问题中提到过了。 - Adam Stelmaszczyk


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

我相信这可能是重写真正的Queue代码以创建一个更简单的例子的遗留物。因为在真正的队列中,你需要照顾 用过的 容量,有时你想允许容量的上限(当消费者无法跟上时阻止生产者)。如果您有这样的代码(具有setCapacity / getCapacity和非最终容量)并通过删除调整大小逻辑和最终确定后备存储来简化它,那么这就是您可能最终得到的结果。


0
2018-06-01 05:58