为啥较小的环形缓冲区破坏器速度较慢?

Posted

技术标签:

【中文标题】为啥较小的环形缓冲区破坏器速度较慢?【英文标题】:Why is disruptor slower with smaller ring buffer?为什么较小的环形缓冲区破坏器速度较慢? 【发布时间】:2017-12-07 04:08:17 【问题描述】:

按照Disruptor Getting Started Guide,我构建了一个具有单个生产者和单个消费者的最小破坏者。

制片人

import com.lmax.disruptor.RingBuffer;

public class LongEventProducer

    private final RingBuffer<LongEvent> ringBuffer;

    public LongEventProducer(RingBuffer<LongEvent> ringBuffer)
    
        this.ringBuffer = ringBuffer;
    

    public void onData()
    
        long sequence = ringBuffer.next();
        try
        
            LongEvent event = ringBuffer.get(sequence);
        
        finally
        
            ringBuffer.publish(sequence);
        
    

消费者(注意消费者什么都不做onEvent

import com.lmax.disruptor.EventHandler;

public class LongEventHandler implements EventHandler<LongEvent>

    public void onEvent(LongEvent event, long sequence, boolean endOfBatch)
    

我的目标是测试绕大环形缓冲区一次与多次遍历较小环形缓冲区的性能。在每种情况下,总操作数 (bufferSize X rotations) 是相同的。我发现随着环形缓冲区变小,ops/sec 速率急剧下降。

RingBuffer Size |  Revolutions  | Total Ops   |   Mops/sec

    1048576     |      1        |  1048576    |     50-60

       1024     |      1024     |  1048576    |     8-16

        64      |      16384    |  1048576    |    0.5-0.7

        8       |      131072   |  1048576    |    0.12-0.14

问题: 当环形缓冲区大小减小但总迭代次数固定时,性能大幅下降的原因是什么? 这种趋势与@无关987654328@ 和Single vs MultiProducer- 吞吐量降低,但趋势相同。

主要(通知SingleProducerBusySpinWaitStrategy

import com.lmax.disruptor.BusySpinWaitStrategy;
import com.lmax.disruptor.dsl.Disruptor;
import com.lmax.disruptor.RingBuffer;
import com.lmax.disruptor.dsl.ProducerType;

import java.util.concurrent.Executor;
import java.util.concurrent.Executors;

public class LongEventMainJava
        static double ONEMILLION = 1000000.0;
        static double ONEBILLION = 1000000000.0;

    public static void main(String[] args) throws Exception 
            // Executor that will be used to construct new threads for consumers
            Executor executor = Executors.newCachedThreadPool();    

            // TUNABLE PARAMS
            int ringBufferSize = 1048576; // 1024, 64, 8
            int rotations = 1; // 1024, 16384, 131702

            // Construct the Disruptor
            Disruptor disruptor = new Disruptor<>(new LongEventFactory(), ringBufferSize, executor, ProducerType.SINGLE, new BusySpinWaitStrategy());

            // Connect the handler
            disruptor.handleEventsWith(new LongEventHandler());

            // Start the Disruptor, starts all threads running
            disruptor.start();

            // Get the ring buffer from the Disruptor to be used for publishing.
            RingBuffer<LongEvent> ringBuffer = disruptor.getRingBuffer();
            LongEventProducer producer = new LongEventProducer(ringBuffer);

            long start = System.nanoTime();
            long totalIterations = rotations * ringBufferSize;
            for (long i = 0; i < totalIterations; i++) 
                producer.onData();
            
            double duration = (System.nanoTime()-start)/ONEBILLION;
            System.out.println(String.format("Buffersize: %s, rotations: %s, total iterations = %s, duration: %.2f seconds, rate: %.2f Mops/s",
                    ringBufferSize, rotations, totalIterations, duration, totalIterations/(ONEMILLION * duration)));
        

要运行,您需要简单的 Factory 代码

import com.lmax.disruptor.EventFactory;

public class LongEventFactory implements EventFactory<LongEvent>

    public LongEvent newInstance()
    
        return new LongEvent();
    

在核心 i5-2400、12GB 内存、Windows 7 上运行

样本输出

Buffersize: 1048576, rotations: 1, total iterations = 1048576, duration: 0.02 seconds, rate: 59.03 Mops/s

Buffersize: 64, rotations: 16384, total iterations = 1048576, duration: 2.01 seconds, rate: 0.52 Mops/s

【问题讨论】:

【参考方案1】:

当生产者填满环形缓冲区时,它必须等到事件被消耗后才能继续。

当你的缓冲区正好是你要放入的元素数量时,生产者永远不必等待。它永远不会溢出。它所做的只是增加计数、索引,并在该索引处发布环形缓冲区中的数据。

当您的缓冲区较小时,它仍然只是增加计数并发布,但它的执行速度比消费者消耗的快。因此生产者必须等到元素被消耗并且环形缓冲区上的空间被释放。

【讨论】:

谢谢。那么为什么在这个例子中是我的消费者,它实际上什么都不做,只是访问底层的LongEvent 与生产者相比如此慢?我原以为制片人会是限制因素。 @AdamHughes 你的部分什么都不做,但Disruptor 基础设施在调用onEvent 方法之前做了一些工作。碰巧这比你的制片人要多。 Trisha 的这篇文章很好地解释了这一点。 mechanitis.blogspot.com/2011/07/…【参考方案2】:

似乎问题在于lmax\disruptor\SingleProducerSequencer中的这段代码

if (wrapPoint > cachedGatingSequence || cachedGatingSequence > nextValue)
        
            cursor.setVolatile(nextValue);  // StoreLoad fence

            long minSequence;
            while (wrapPoint > (minSequence = Util.getMinimumSequence(gatingSequences, nextValue)))
            
                waitStrategy.signalAllWhenBlocking();
                LockSupport.parkNanos(1L); // TODO: Use waitStrategy to spin?
            

            this.cachedValue = minSequence;
        

尤其是对LockSupport.parkNanos(1L) 的调用。这最多可能需要15ms on Windows。当生产者到达缓冲区的末尾并等待消费者时,就会调用它。

其次,当缓冲区较小时,很可能会发生 RingBuffer 的错误共享。我猜这两种效果都在起作用。

最后,我能够在基准测试之前使用 JIT 对 onData() 进行 100 万次调用来加速代码。这得到了最好的情况 &gt; 80Mops/sec,但并没有消除缓冲区收缩带来的退化。

【讨论】:

以上是关于为啥较小的环形缓冲区破坏器速度较慢?的主要内容,如果未能解决你的问题,请参考以下文章

数据结构之环形缓冲器

linux内核之Kfifo环形队列

环形缓冲区

怎么计算环形缓冲区长度

环形缓冲区大小和反写阈值

微控制器的环形缓冲区