解读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属性外的干扰。

/*
 * 填充辅助类,为解决缓存的伪共享问题,需要对每个缓存行(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
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,298评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,701评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,078评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,687评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,018评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,410评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,729评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,412评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,124评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,379评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,903评论 1 257
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,268评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,894评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,014评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,770评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,435评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,312评论 2 260

推荐阅读更多精彩内容