Featured image of post Disruptor高性能分析

Disruptor高性能分析

Disruptor高性能分析

Disruptor是高性能的无锁有界内存队列。日志框架Log4j2也正是利用Disruptor带来的极大的性能提升。

1.内存分配

使用RingBuffer数据结构,本质 还是数组,数组元素在初始化的时候一次性创建,提高缓存命中率;对象循环利用,避免频繁垃圾回收。

因为数组元素是初始化一次性创建,所以数组元素分配的内存大概率也是连续内存。很好的利用了局部性原理,充分利用CPU缓存。

 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
    RingBufferFields(
        EventFactory<E> eventFactory,
        Sequencer sequencer)
    {
        this.sequencer = sequencer;
        this.bufferSize = sequencer.getBufferSize();

        if (bufferSize < 1)
        {
            throw new IllegalArgumentException("bufferSize must not be less than 1");
        }
        if (Integer.bitCount(bufferSize) != 1)
        {
            throw new IllegalArgumentException("bufferSize must be a power of 2");
        }

        this.indexMask = bufferSize - 1;
        this.entries = new Object[sequencer.getBufferSize() + 2 * BUFFER_PAD];
        fill(eventFactory);
    }

    private void fill(EventFactory<E> eventFactory)
    {
        for (int i = 0; i < bufferSize; i++)
        {
            entries[BUFFER_PAD + i] = eventFactory.newInstance();
        }
    }

RingBufferFieldsRingBuffer的抽象父类,RingBuffer的构造方法是直接调用父类的构造方法。

可以在源码中发现RingBufferFields在初始化的时候就已经对数组的每一个元素都填充了Event,这里的Event对象并不是一个事件new一个Event对象,而是新事件直接替换Event的属性值。Event类是需要自定义实现的。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
    public class Event {
        /**
         * 事件
         */
        private E event;

        // 设置事件的值
        public void setValue(E event) {
            this.event = event;
        }
        // 重写 toString 方法,用于调试时打印事件信息
        @Override
        public String toString() {
            return "Event{" + "event=" + event + '}';
        }
    }

连续内存,这里就能很好的利用空间局部性原理:某一块内存一旦被访问,不久之后这块内存附近的内存也很有可能被访问,而CPU的缓存在设计的时候也正是利用了局部性原理。同时消费者在消费的时候,也会按照这种连续的顺序去消费事件,那么如果这些连续的事件的内存空间也是连续的,就能够充分利用到空间局部性原理,消费的时候大概率能直接从缓冲中读取到,而不用再从内存中去加载。

缓存的的读写速率比内存高很多倍,CPU缓存设计的初衷就是解决处理器与内存之间的速度差异所带来的性能瓶颈

2.避免伪共享

伪共享问题:

伪共享(False Sharing)是一种与多线程编程和CPU缓存相关的性能问题。它指的是多个线程同时访问不同但位于同一缓存行(Cache Line)的数据,导致频繁的缓存失效和数据同步操作,从而降低了程序的性能。

CPU缓存以缓存行为单位进行读取和写入操作,一般缓存行的大小为64字节(在大多数情况下)。当多个线程同时访问不同的变量,但这些变量位于同一个缓存行时,由于缓存一致性协议的限制,每次一个线程更新缓存行中的某个变量时,会导致该缓存行的所有数据都失效,其他线程访问缓存行中的其他变量时,需要重新从主内存加载数据,造成额外的缓存失效和数据同步开销。

伪共享问题可能导致程序性能显著下降,特别是在多核处理器上运行多线程程序时,由于多个线程之间的竞争和数据同步带来的开销。

Disruptor解决伪共享问题:

通过特殊的结构,很好的避免的伪共享问题。提升缓存的命中率。

可已在Sequence的源码中发现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
class LhsPadding
{
    protected long p1, p2, p3, p4, p5, p6, p7;
}

class Value extends LhsPadding
{
    protected volatile long value;
}

class RhsPadding extends Value
{
    protected long p9, p10, p11, p12, p13, p14, p15;
}

public class Sequence extends RhsPadding{
    //`````
}

Sequence的value属性值可以看到原本8个字节的value,在前后都嵌入了7个long类型,value前面56字节,后面也是56个字节。因为缓存是按照缓存行进行缓存的读取和移除的,大部分处理器的缓存行都是按照64字节为一个缓存行,这样的设计就能保证value必定能够独享一个缓存行,也就避免的伪共享问题。但是可以看到这种方式内存浪费是很严重的。

3.无锁算法

通过无锁算法,避免了频繁的加锁解锁带来的性能开销。

核心就是CAS,和无限循环。

参考资料:

https://ifeve.com/disruptor-cacheline-padding/

Licensed under CC BY-NC-SA 4.0