在"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
快得多,但我失败了。
马丁说两个场景出现了问题:
答案 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
的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和非最终容量)并通过删除调整大小逻辑和最终确定后备存储来简化它,那么这就是您可能最终得到的结果。