我的disruptor-net代码比BlockingCollection慢

时间:2012-11-15 14:39:29

标签: c# disruptor-pattern

Disruptor应该比BlockingCollection快得多。

在我之前的问题中, Why is my disruptor example so slow? 我写了两个测试。 Disruptor花了大约1微秒(或更少),而BlockingCollection花了大约14微秒。

所以我决定在我的程序中使用Disruptor,但是当我实现它时,我发现现在Disruptor花费50微秒,而BlockingCollection仍然花费14-18微秒。

我已将我的生产代码修改为“独立测试”,Disruptor仍然花费50微秒。为什么呢?

简化测试如下。在这个测试中,我有两个选择。第一个选项是Sleep for 1 ms。然后Disruptor花费30-50微秒来交付。第二个选项是模拟活动。然后Disruptor花费7微秒来交付。与BlockingCollection相同的测试结果为14-18微秒。那么为什么Disruptor不比BlockingCollection快?

在我的真实应用程序Disruptor花费50微秒来提供太多的东西!我希望它能以比1微秒更快的速度传递信息。

using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using Disruptor;

namespace DisruptorTest
{
    public sealed class ValueEntry
    {
        internal int Id { get; set; }
    }

    class DisruptorTest
    {

        public class MyHandler : IEventHandler<ValueEntry>
        {
            private DisruptorTest _parent;

            public MyHandler(DisruptorTest parent)
            {
                this._parent = parent;
            }

            public void OnNext(ValueEntry data, long sequence, bool endOfBatch)
            {
                _parent.sw.Stop();
                long microseconds = _parent.sw.ElapsedTicks / (Stopwatch.Frequency / (1000L * 1000L));

                // Filter out abnormal delays > 1000
                if (microseconds < 1000)
                {
                    _parent.sum += (int)microseconds;
                    _parent.count++;
                    if (_parent.count % 1000 == 0)
                    {
                        Console.WriteLine("average disruptor delay (microseconds) = {0}", _parent.sum / _parent.count);
                    }
                }
            }
        }

        private RingBuffer<ValueEntry> _ringBuffer;
        private const int RingSize = 64;

        static void Main(string[] args)
        {
            new DisruptorTest().Run();
        }

        public void Run()
        {
            var disruptor = new Disruptor.Dsl.Disruptor<ValueEntry>(() => new ValueEntry(), RingSize, TaskScheduler.Default);
            disruptor.HandleEventsWith(new MyHandler(this));

            _ringBuffer = disruptor.Start();

            for (int i = 0; i < 10001; i++)
            {
                Do();

                // We need to simulate activity to allow event to deliver

                // Option1. just Sleep. Result 30-50 microseconds.
                Thread.Sleep(1);

                // Option2. Do something. Result ~7 microseconds.
                //factorial = 1;
                //for (int j = 1; j < 100000; j++)
                //{
                //    factorial *= j;
                //}
            }
        }

        public static int factorial;

        private Stopwatch sw = Stopwatch.StartNew();
        private int sum;
        private int count;

        public void Do()
        {
            long sequenceNo = _ringBuffer.Next();
            _ringBuffer[sequenceNo].Id = 0;
            sw.Restart();
            _ringBuffer.Publish(sequenceNo);
        }

    }
}

旧代码。现在应该被忽略:

using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using Disruptor;

namespace DisruptorTest
{
    public sealed class ValueEntry
    {
        internal int Id { get; set; }
    }

    class DisruptorTest
    {

        public class MyHandler : IEventHandler<ValueEntry>
        {
            private readonly int _ordinal;
            private readonly int _consumers;
            private DisruptorTest _parent;

            public MyHandler(int ordinal, int consumers, DisruptorTest parent)
            {
                _ordinal = ordinal;
                _consumers = consumers;
                this._parent = parent;
            }

            public void OnNext(ValueEntry data, long sequence, bool endOfBatch)
            {
                if ((sequence % _consumers) == _ordinal)
                {
                    var id = data.Id;
                    _parent.sw[id].Stop();
                    long microseconds = _parent.sw[id].ElapsedTicks / (Stopwatch.Frequency / (1000L * 1000L));
                    // filter out abnormal delays > 1000
                    if (microseconds < 1000)
                    {
                        _parent.sum[id] += (int)microseconds;
                        _parent.count[id]++;
                        if (_parent.count[id] % 10 == 0)
                        {
                            Console.WriteLine("Id = {0} average disruptor delay (microseconds) = {1}",
                                id, _parent.sum[id] / _parent.count[id]);
                        }
                    }
                }
            }
        }

        private const int NumberOfThreads = 1;
        private RingBuffer<ValueEntry> _ringBuffer;
        private const int RingSize = 64;

        static void Main(string[] args)
        {
            new DisruptorTest().Run();
        }

        public void Run()
        {
            var disruptor = new Disruptor.Dsl.Disruptor<ValueEntry>(() => new ValueEntry(), RingSize, TaskScheduler.Default);
            for (int i = 0; i < NumberOfThreads; i++)
                disruptor.HandleEventsWith(new MyHandler(i, NumberOfThreads, this));

            for (int i = 0; i < sw.Length; i++)
            {
                sw[i] = Stopwatch.StartNew();
            }

            _ringBuffer = disruptor.Start();

            //var rnd = new Random();
            for (int i = 0; i < 1000; i++)
            {
                //Do(rnd.Next(MaxId));
                Do(i % MaxId);
                Thread.Sleep(1);
            }
        }

        private const int MaxId = 100;

        private Stopwatch[] sw = new Stopwatch[MaxId];
        private int[] sum = new int[MaxId];
        private int[] count = new int[MaxId];

        public void Do(int id)
        {
            long sequenceNo = _ringBuffer.Next();
            _ringBuffer[sequenceNo].Id = id;
            sw[id].Restart();
            _ringBuffer.Publish(sequenceNo);
        }

    }
}

输出:

......
Id = 91 average disruptor delay (microseconds) = 50
Id = 92 average disruptor delay (microseconds) = 48
Id = 93 average disruptor delay (microseconds) = 35
Id = 94 average disruptor delay (microseconds) = 35
Id = 95 average disruptor delay (microseconds) = 51
Id = 96 average disruptor delay (microseconds) = 55
Id = 97 average disruptor delay (microseconds) = 38
Id = 98 average disruptor delay (microseconds) = 37
Id = 99 average disruptor delay (microseconds) = 45

2 个答案:

答案 0 :(得分:4)

您仍然在做同样的事情:您正在测量发布单个项目所需的时间。

public void Do(int id)
{
    long sequenceNo = _ringBuffer.Next();
    _ringBuffer[sequenceNo].Id = id;
    sw[id].Restart(); // <--- You're doing this EVERY TIME YOU PUBLISH an item!
    _ringBuffer.Publish(sequenceNo);
}

在上一个问题中,您被告知您应该测量数百个出版物,以便正确使用Stopwatch精度。

此外,您还在测试过程中写入控制台。避免这样做:

if (_parent.count[id] % 10 == 0)
{
    Console.WriteLine("Id = {0} average disruptor delay (microseconds) = {1}",
        id, _parent.sum[id] / _parent.count[id]);
}

清理您的代码

至少,你应该尝试清理你的代码;我重新组织了一下,所以它不是那么混乱:http://pastie.org/5382971

Disrputors开始并不那么简单,现在我们必须处理您的代码并尝试告诉您如何解决它。更重要的是:当你有意大利面条代码时,你无法进行性能优化或测试。尽量保持一切简单和干净。在这个阶段,您的代码既不简单也不干净。

让我们从您的私有成员变量的可怕命名约定开始:

private const int NumberOfThreads = 1;
private RingBuffer<ValueEntry> _ringBuffer;
private const int RingSize = 64;
private const int MaxId = 100
private Stopwatch[] sw = new Stopwatch[MaxId];
private int[] sum = new int[MaxId];
private int[] count = new int[MaxId];

保持一致:

private const int _numberOfThreads = 1;
private RingBuffer<ValueEntry> _ringBuffer;
private const int _ringSize = 64;
private const int _maxId = 100
private Stopwatch[] _sw = new Stopwatch[MaxId];
private int[] _sum = new int[MaxId];
private int[] _count = new int[MaxId];

其他一些指示:

  • 摆脱嵌套类。
  • 将主要移动到单独的类(例如程序)。

建立良好的测试

Martin和Michael告诉你的第一件事就是性能测试也必须非常好,所以他们花了很多时间构建testing framework

  • 我建议您尝试几百万个事件,而不是1000个事件。
  • 确保您只为所有活动使用一个计时器。
  • 开始处理项目时启动计时器,当没有其他项目需要处理时停止计时。
  • 了解处理完项目的有效方法是使用CountDownEvent

更新

所以让我们先解决第一个争议:the precision of the stopwatch should indeed be sufficient.

Int64 frequency = Stopwatch.Frequency;
Console.WriteLine( "  Timer frequency in ticks per second = {0}", frequency );
Int64 nanosecPerTick = (1000L * 1000L * 1000L) / frequency;
Console.WriteLine( "  Timer is accurate within {0} nanoseconds", nanosecPerTick );

在我的机器上,分辨率在320纳秒之内。所以OP是正确的,定时器的分辨率应该不是问题。

我知道OP希望衡量一个项目的平均交付量,但至少有两种方法可以做到。

我们必须调查差异。在概念层面上,您正在执行以下代码:

  1. 你正在运行一堆迭代。
  2. 测量每一个。
  3. 您计算总数。
  4. 你计算最后的平均值。
  5. 在代码中:

    Stopwatch sw = new Stopwatch();
    long totalMicroseconds = 0;
    int numItems = 1000;
    for(int i = 0; i < numItems; i++)
    {
        sw.Reset();
        sw.Start();
        OneItemDelivery();
        sw.Stop();
        totalMicroseconds += sw.ElapsedTicks / (Stopwatch.Frequency / (1000L * 1000L));
    }
    long avgOneItemDelivery = totalMicroseconds/numItems;
    

    测量性能的另一种方法是:

    1. 启动计时器。
    2. 运行所有迭代。
    3. 停止计时器。
    4. 计算平均时间。
    5. 在代码中:

      sw.Start();
      for(int i = 0; i < numItems; i++)
      {
          OneItemDelivery();    
      }
      sw.Stop();
      totalMicroseconds = sw.ElapsedTicks / (Stopwatch.Frequency / (1000L * 1000L));
      long avgOneItemDelivery = totalMicroseconds/numItems;
      

      每个人都有自己的问题:

      • 第一种方法可能不太精确,您需要在您的系统上证明秒表可以精确处理少量工作(除了简单地计算纳秒精度)。
      • 第二种方法还将包括迭代发生所需的计算时间。这会在测量中引入少量偏差,但它可以抵消第一种方法通常会出现的精度问题。

      您已经注意到Sleep语句会降低性能,因此我建议您进行简单的计算。计算阶乘似乎是一个好主意,只需要进行一个非常小的计算:不需要100000,100也应该没问题。

      当然,你不需要等待2分钟进行测试,但10-20秒不应该是一个问题。

答案 1 :(得分:0)

我读了您从Why is my disruptor example so slow?编写的BlockingCollecton代码,您在Disruptor中添加了许多Console.WriteLine,但在BlockingCollection中却没有一个,Console.WriteLine很慢,里面有一个锁。

您的RingBufferSize太小,此效果会影响性能,应为1024或更大。

while (!dataItems.IsCompleted)可能有问题,BlockCollection始终处于添加状态,这将导致线程提早结束。

Task.Factory.StartNew(() => {
    while (!dataItems.IsCompleted)
    {

        ValueEntry ve = null;
        try
        {
    ve = dataItems.Take();
    long microseconds = sw[ve.Value].ElapsedTicks / (Stopwatch.Frequency / (1000L * 1000L));
    results[ve.Value] = microseconds;

    //Console.WriteLine("elapsed microseconds = " + microseconds);
    //Console.WriteLine("Event handled: Value = {0} (processed event {1}", ve.Value, ve.Value);
        }
        catch (InvalidOperationException) { }
    }
}, TaskCreationOptions.LongRunning);


for (int i = 0; i < length; i++)
{
    var valueToSet = i;

    ValueEntry entry = new ValueEntry();
    entry.Value = valueToSet;

    sw[i].Restart();
    dataItems.Add(entry);

    //Console.WriteLine("Published entry {0}, value {1}", valueToSet, entry.Value);
    //Thread.Sleep(1000);
}

我已经为您重写了代码,Disruptor比具有多生产者(10个并行生产者)的BlockingCollection快10倍,比具有单一生产者的BlockingCollection快2倍:

using System;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using Disruptor;
using Disruptor.Dsl;
using NUnit.Framework;

namespace DisruptorTest.Ds
{
    public sealed class ValueEntry
    {
        internal int Id { get; set; }
    }

    class MyHandler : IEventHandler<ValueEntry>
    {
        public void OnEvent(ValueEntry data, long sequence, bool endOfBatch)
        {
        }
    }

    [TestFixture]
    public class DisruptorPerformanceTest
    {
        private volatile bool collectionAddEnded;

        private int producerCount = 10;
        private int runCount = 1000000;
        private int RingBufferAndCapacitySize = 1024;

        [TestCase()]
        public async Task TestBoth()
        {
            for (int i = 0; i < 1; i++)
            {
                foreach (var rs in new int[] {64, 512, 1024, 2048 /*,4096,4096*2*/})
                {
                    Console.WriteLine($"RingBufferAndCapacitySize:{rs}, producerCount:{producerCount}, runCount:{runCount} of {i}");
                    RingBufferAndCapacitySize = rs;
                    await DisruptorTest();
                    await BlockingCollectionTest();
                }
            }
        }

        [TestCase()]
        public async Task BlockingCollectionTest()
        {
            var sw = new Stopwatch();
            BlockingCollection<ValueEntry> dataItems = new BlockingCollection<ValueEntry>(RingBufferAndCapacitySize);

            sw.Start();

            collectionAddEnded = false;

            // A simple blocking consumer with no cancellation.
            var task = Task.Factory.StartNew(() =>
            {
                while (!collectionAddEnded && !dataItems.IsCompleted)
                {
                    //if (!dataItems.IsCompleted && dataItems.TryTake(out var ve))
                    if (dataItems.TryTake(out var ve))
                    {
                    }
                }
            }, TaskCreationOptions.LongRunning);


            var tasks = new Task[producerCount];
            for (int t = 0; t < producerCount; t++)
            {
                tasks[t] = Task.Run(() =>
                {
                    for (int i = 0; i < runCount; i++)
                    {
                        ValueEntry entry = new ValueEntry();
                        entry.Id = i;
                        dataItems.Add(entry);
                    }
                });
            }

            await Task.WhenAll(tasks);

            collectionAddEnded = true;
            await task;

            sw.Stop();

            Console.WriteLine($"BlockingCollectionTest Time:{sw.ElapsedMilliseconds/1000d}");
        }


        [TestCase()]
        public async Task DisruptorTest()
        {
            var disruptor =
                new Disruptor.Dsl.Disruptor<ValueEntry>(() => new ValueEntry(), RingBufferAndCapacitySize, TaskScheduler.Default,
                    producerCount > 1 ? ProducerType.Multi : ProducerType.Single, new BlockingWaitStrategy());
            disruptor.HandleEventsWith(new MyHandler());

            var _ringBuffer = disruptor.Start();

            Stopwatch sw = Stopwatch.StartNew();

            sw.Start();


            var tasks = new Task[producerCount];
            for (int t = 0; t < producerCount; t++)
            {
                tasks[t] = Task.Run(() =>
                {
                    for (int i = 0; i < runCount; i++)
                    {
                        long sequenceNo = _ringBuffer.Next();
                        _ringBuffer[sequenceNo].Id = 0;
                        _ringBuffer.Publish(sequenceNo);
                    }
                });
            }


            await Task.WhenAll(tasks);


            disruptor.Shutdown();

            sw.Stop();
            Console.WriteLine($"DisruptorTest Time:{sw.ElapsedMilliseconds/1000d}s");
        }
    }
}

具有共享ValueEntry实例的BlockingCollectionTest(for循环中没有新的ValueEntry())

  • RingBufferAndCapacitySize:64,producerCount:10,runCount:1000000 of 0

    DisruptorTest时间:16.962s

    BlockingCollectionTest时间:18.399

  • RingBufferAndCapacitySize:512,producerCount:10,runCount:1000000为0 DisruptorTest时间:6.101s

    BlockingCollectionTest时间:19.526

  • RingBufferAndCapacitySize:1024,producerCount:10,runCount:1000000 of 0

    DisruptorTest时间:2.928秒

    BlockingCollectionTest时间:20.25

  • RingBufferAndCapacitySize:2048,producerCount:10,runCount:1000000 of 0

    DisruptorTest时间:2.448秒

    BlockingCollectionTest时间:20.649

BlockingCollectionTest在for循环中创建一个新的ValueEntry()

  • RingBufferAndCapacitySize:64,producerCount:10,runCount:1000000 of 0

    DisruptorTest时间:27.374s

    BlockingCollectionTest时间:21.955

  • RingBufferAndCapacitySize:512,producerCount:10,runCount:1000000 of 0

    DisruptorTest时间:5.011秒

    BlockingCollectionTest时间:20.127

  • RingBufferAndCapacitySize:1024,producerCount:10,runCount:1000000 of 0

    DisruptorTest时间:2.877秒

    BlockingCollectionTest时间:22.656

  • RingBufferAndCapacitySize:2048,producerCount:10,runCount:1000000 of 0

    DisruptorTest时间:2.384秒

    BlockingCollectionTest时间:23.567

https://www.cnblogs.com/darklx/p/11755686.html