今天和大家聊一聊Disruptor中的RingBuffer。代码版本基于3.3.6,逻辑和3.4.x变化不大。
0x01 Disruptor中的RingBuffer
RingBuffer在Disruptor早期功能比较多,承载着数据存储、生产消费的数据交换等任务。现在只保留了存储的能力,像发布数据这些功能也只是通过调用Sequencer去实现的。在RingBuffer中其实看不到这个"Buffer"为何是"Ring",可以看看我之前关于生产者的文章了解下。
这里我们可以把Disruptor中的RingBuffer简单地理解为一个经过特殊优化的数组。
这个“特殊的数组”的特别之处在于:
- 尽可能消除缓存的伪共享问题;
- 使用数组存储,预先分配(尽可能)连续的内存地址,非常适合FIFO的时序消息特性,充分利用CPU Cache预取能力;
- 对象重用,减少不必要的GC;
0x02 实现细节
0x02.1 解决伪共享问题
伪共享简介:计算机缓存是以缓存行(cache line)大小从内存拉数据并存储。缓存行最常见大小是64个字节。当同一缓存行内的不同变量被不同,就会无意中影响彼此的性能,这就是伪共享。伪共享常被称做无声的性能杀手,因为从代码中很难看清楚是否会出现伪共享。
RingBuffer类图类图最左侧的RingBufferPad、RingBufferFields就是为了解决伪共享问题的。在RingBuffer的生命周期中,RingBufferFields中的属性会被频繁访问,为了解决缓存的伪共享问题,需要对每个缓存行进行填充。这种形式在Disruptor中经常使用。下表是RingBuffer实例属性。可以发现,不管缓存行中从哪个位置加载代表RingBuffer实例的数据,实际使用的属性sequencer、bufferSize、entries、indexMask会被加载到一或两个缓存行中,不会受到非RingBuffer属性外的干扰。
/*
* 填充辅助类,为解决缓存的伪共享问题,需要对每个缓存行(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的一贯思想,为了追求极致性能,不得不在软件层做出对硬件层的妥协。
更多的源码注释参考
网友评论