解读Disruptor系列--解读源码(4)之RingBuffer

今天和大家聊一聊Disruptor中的RingBuffer。代码版本基于3.3.6,逻辑和3.4.x变化不大。

0x01 Disruptor中的RingBuffer

RingBuffer在Disruptor早期功能比较多,承载着数据存储、生产消费的数据交换等任务。现在只保留了存储的能力,像发布数据这些功能也只是通过调用Sequencer去实现的。在RingBuffer中其实看不到这个”Buffer”为何是”Ring”,可以看看我之前关于生产者的文章了解下。
这里我们可以把Disruptor中的RingBuffer简单地理解为一个经过特殊优化的数组。
这个“特殊的数组”的特别之处在于:

  1. 尽可能消除缓存的伪共享问题;
  2. 使用数组存储,预先分配(尽可能)连续的内存地址,非常适合FIFO的时序消息特性,充分利用CPU Cache预取能力;
  3. 对象重用,减少不必要的GC;

    0x02 实现细节

    0x02.1 解决伪共享问题

    伪共享简介:计算机缓存是以缓存行(cache line)大小从内存拉数据并存储。缓存行最常见大小是64个字节。当同一缓存行内的不同变量被不同,就会无意中影响彼此的性能,这就是伪共享。伪共享常被称做无声的性能杀手,因为从代码中很难看清楚是否会出现伪共享。

RingBuffer类图

类图最左侧的RingBufferPad、RingBufferFields就是为了解决伪共享问题的。在RingBuffer的生命周期中,RingBufferFields中的属性会被频繁访问,为了解决缓存的伪共享问题,需要对每个缓存行进行填充。这种形式在Disruptor中经常使用。下表是RingBuffer实例属性。可以发现,不管缓存行中从哪个位置加载代表RingBuffer实例的数据,实际使用的属性sequencer、bufferSize、entries、indexMask会被加载到一或两个缓存行中,不会受到非RingBuffer属性外的干扰。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
/*
* 填充辅助类,为解决缓存的伪共享问题,需要对每个缓存行(64B)进行填充
*/
abstract class RingBufferPad
{ // https://github.com/LMAX-Exchange/disruptor/issues/167
/*
RingBufferFields中的属性被频繁读取,这里的属性是为了避免RingBufferFields遇到伪共享问题
*/
protected long p1, p2, p3, p4, p5, p6, p7;
}
abstract class RingBufferFields<E> extends RingBufferPad {
private static final int BUFFER_PAD; // 用于在数组中进行缓存行填充的空元素个数
private static final long REF_ARRAY_BASE; // 内存中引用数组的开始元素基地址,是数组开始的地址+BUFFER_PAD个元素的偏移量之和,后续元素的内存地址需要在此基础计算地址
private static final int REF_ELEMENT_SHIFT; // 引用元素的位移量,用于计算BUFFER_PAD偏移量,基于位移计算比乘法运算更高效
private static final Unsafe UNSAFE = Util.getUnsafe(); // 上面的变量都是为了UNSAFE的操作
static {
final int scale = UNSAFE.arrayIndexScale(Object[].class); // arrayIndexScale获取数组中一个元素占用的字节数,不同JVM实现可能有不同的大小
if (4 == scale) {
REF_ELEMENT_SHIFT = 2;
} else if (8 == scale) {
REF_ELEMENT_SHIFT = 3;
} else {
throw new IllegalStateException("Unknown pointer size");
}
BUFFER_PAD = 128 / scale; // BUFFER_PAD=32 or 16,为什么是128呢?是为了满足处理器的缓存行预取功能(Adjacent Cache-Line Prefetch)
// https://github.com/LMAX-Exchange/disruptor/issues/158
// https://software.intel.com/en-us/articles/optimizing-application-performance-on-intel-coret-microarchitecture-using-hardware-implemented-prefetchers
// Including the buffer pad in the array base offset
// BUFFER_PAD << REF_ELEMENT_SHIFT 实际上是BUFFER_PAD * scale的等价高效计算方式
REF_ARRAY_BASE = UNSAFE.arrayBaseOffset(Object[].class) + (BUFFER_PAD << REF_ELEMENT_SHIFT);
}
private final long indexMask; // 用于进行 & 位与操作,实现高效的模操作
private final Object[] entries;
protected final int bufferSize;
protected final Sequencer sequencer; // 生产者序列号
// 省略...
}
public final class RingBuffer<E> extends RingBufferFields<E> implements Cursored, EventSequencer<E>, EventSink<E>
{
public static final long INITIAL_CURSOR_VALUE = Sequence.INITIAL_VALUE; // 游标初始值 -1
protected long p1, p2, p3, p4, p5, p6, p7;
}
属性类型 属性名 字节数
long p7 8
long p6 8
long p5 8
long p4 8
long p3 8
long p2 8
long p1 8
ref sequencer 4/8
int bufferSize 4
ref entries 4/8
long indexMask 8
long p7 8
long p6 8
long p5 8
long p4 8
long p3 8
long p2 8
long p1 8

0x02.2 对象复用与缓存预取

使用数组而非链表,可以通过数组连续内存的特性最大化利用缓存行。但是数组里存储的一般是对象的引用,所以提前初始化对象有两点好处,其一是避免频繁的创建销毁,减少young gc,其二是通过初始化所有对象,尽可能使对象内存连续,由于处理器通常开启了缓存预取机制(参见Intel缓存预取文章),这样就增加了缓存效率,降低了整体时延。Disruptor使用的事件对象在RingBuffer中不断往前推进,缓存可能在使用前就将数据准备好了。

0x03 总结

RingBuffer的设计秉承了Disruptor的一贯思想,为了追求极致性能,不得不在软件层做出对硬件层的妥协。

更多的源码注释参考

参考资料

  1. 细说Cache-L1/L2/L3/TLB
  2. Why software developers should care about CPU caches
  3. Optimizing Application Performance on Intel® Core™ Microarchitecture Using Hardware-Implemented Prefetchers
  4. CPU Cache Wiki
  5. Locality of reference Wiki