在里面 “95%的性能是关于干净的代表性模型” 通过谈话 马丁汤普森在17到21分钟之间,会出现以下代码:
public class Queue
{
private final Object[] buffer;
private final int capacity;
// Rest of the code
}
在20:16他说:
你可以获得更好的性能,所以留下类似的东西 capacity
在这是正确的事情。
我试着想出一个代码示例 capacity
会快得多 buffer.length
但是我失败了。
马丁说两个场景中出现问题:
- 在一个并发的世界。 但,
length
场也是 final
, JLS 10.7。所以,我不知道这可能是一个什么问题。
- 当缓存未命中时。 一世 试着 调用
capacity
VS buffer.length
一百万次(队列中有一百万个元素),但没有显着差异。我使用JMH进行基准测试。
能否请您提供一个代码示例,其中演示了一个案例 capacity
优于 buffer.length
在表现方面?
更常见的情况(经常在实际代码中发现)越好。
请注意,我完全取消了美学,清洁代码,代码重新分解等方面的内容。我只询问性能。
正常访问数组时,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
你不应该直接克服马丁的话。当他说“使用 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,顺便说一句)将此优化为死亡。
我不是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。
非常有兴趣了解这个问题的更多信息。
我怀疑这会对性能产生任何积极影响。例如,它无法帮助消除Hotspot中的绑定检查。更糟: 它可能在一个JVM中更快,但可能在下一个版本中会受到伤害。 Java不断获得额外的优化,并且数组边界检查是他们努力优化的一件事......
我相信这可能是重写真正的Queue代码以创建一个更简单的例子的遗留物。因为在真正的队列中,你需要照顾 用过的 容量,有时你想允许容量的上限(当消费者无法跟上时阻止生产者)。如果您有这样的代码(具有setCapacity / getCapacity和非最终容量)并通过删除调整大小逻辑和最终确定后备存储来简化它,那么这就是您可能最终得到的结果。