.NET 4.0 Производительность одновременной сборки

Я пытаюсь написать программу, в которой я планирую элементы для удаления, помещая их в коллекцию из разных потоков и очищая их в одном потоке, который выполняет итерацию коллекции и размещает элементы.

Прежде чем делать это, я задавался вопросом, что даст оптимальную производительность, поэтому я пробовал ConcurrentBag, ConcurrentStack и ConcurrentQueue и измерял время, необходимое для добавления 10000000 элементов.

Я использовал следующую программу для тестирования:

class Program
{
    static List<int> list = new List<int>();
    static ConcurrentBag<int> bag = new ConcurrentBag<int>();
    static ConcurrentStack<int> stack = new ConcurrentStack<int>();
    static ConcurrentQueue<int> queue = new ConcurrentQueue<int>();
    static void Main(string[] args)
    {
        run(addList);
        run(addBag);
        run(addStack);
        run(addQueue);
        Console.ReadLine();
    }

    private static void addList(int obj) { lock (list) { list.Add(obj); } }

    private static void addStack(int obj) { stack.Push(obj); }

    private static void addQueue(int obj) { queue.Enqueue(obj); }

    private static void addBag(int obj) { bag.Add(obj); }



    private static void run(Action<int> action)
    {
        Stopwatch stopwatch = Stopwatch.StartNew();
        Parallel.For(0, 10000000, new ParallelOptions() { MaxDegreeOfParallelism = # }, action);
        stopwatch.Stop();
        Console.WriteLine(action.Method.Name + " takes " + stopwatch.Elapsed);
    }
}

где # - количество используемых потоков.

но результаты довольно запутанны:

с 8 потоками:

  • addList принимает 00: 00: 00.8166816
  • addBag занимает 00: 00: 01.0368712
  • addStack принимает 00: 00: 01.0902852
  • addQueue принимает 00: 00: 00.6555039

с 1 потоком:

  • addList принимает 00: 00: 00.3880958
  • addBag принимает 00: 00: 01.5850249
  • addStack принимает 00: 00: 01.2764924
  • addQueue принимает 00: 00: 00.4409501

поэтому, независимо от того, сколько потоков, кажется, что просто блокировка простого старого списка быстрее, чем использование любой из параллельных коллекций, за исключением, может быть, очереди, если ему нужно обрабатывать много записей.

EDIT: после комментариев ниже о сборке мусора и отладки: Да, это влияет на бенчмарк. Влияние отладки сборки будет линейным, Garbage будет увеличиваться с увеличением использования памяти.

Однако выполнение одного и того же теста несколько раз дает примерно одинаковые результаты.

Я перенес инициализацию коллекции прямо перед тестовым запуском и собираю мусор после запуска сейчас, например:

        list = new List<int>();
        run(addList);
        list = null;
        GC.Collect();

с MaxDegreeOfParallelism, установленным в 8, я получаю следующие результаты:

  • addList принимает 00: 00: 7959546
  • addBag принимает 00: 00: 01.08023823
  • addStack принимает 00: 00: 01.1354566
  • addQueue принимает 00: 00: 00.6597145

с отклонением 0,02 секунды каждый раз, когда я запускаю код.

Ответ 1

Совпадающие коллекции не всегда быстрее. многие из них видят только первичную прибыль на более высоких уровнях конкуренции, и фактическая рабочая нагрузка также влияет. Посмотрите эту статью из команды pfx:)

http://blogs.msdn.com/b/pfxteam/archive/2010/04/26/9997562.aspx

Остерегайтесь преждевременной оптимизации. поместите что-то вместе, что работает, а затем оптимизируйте. тем более что фактическая рабочая нагрузка важна. Кроме того, наличие замков в качестве основного узкого места является довольно посудой, обычно существует некоторый io или другой алгоритм, который занимает гораздо больше времени.

Ответ 2

Не забывайте, что вам также не нужно добавлять элементы в коллекцию, но также нужно их восстановить. Таким образом, более справедливое сравнение между Monitor на основе Queue <T> , и BlockingCollection <T> , каждый из которых имеет 8 производителей и 1 потребитель.

Затем я получаю следующие результаты на моей машине (я увеличил число итераций в 10 раз):

  • AddQueue1 принимает 00: 00: 18.0119159
  • AddQueue2 принимает 00: 00: 13.3665991

Но это не только интересная производительность. Взгляните на два подхода: очень сложно проверить Add/ConsumeQueue1 на правильность, в то время как очень легко увидеть, что Add/ConsumeQueue2 точно выполняет то, что предназначено благодаря абстракции, предоставляемой BlockingCollection < Т > .


static Queue<int> queue1 = new Queue<int>();
static BlockingCollection<int> queue2 = new BlockingCollection<int>();

static void Main(string[] args)
{
    Run(AddQueue1, ConsumeQueue1);
    Run(AddQueue2, ConsumeQueue2);
    Console.ReadLine();
}

private static void AddQueue1(int obj)
{
    lock (queue1)
    {
        queue1.Enqueue(obj);
        if (queue1.Count == 1)
            Monitor.Pulse(queue1);
    }
}

private static void ConsumeQueue1()
{
    lock (queue1)
    {
        while (true)
        {
            while (queue1.Count == 0)
                Monitor.Wait(queue1);
            var item = queue1.Dequeue();
            // do something with item
        }
    }
}

private static void AddQueue2(int obj)
{
    queue2.TryAdd(obj);
}

private static void ConsumeQueue2()
{
    foreach (var item in queue2.GetConsumingEnumerable())
    {
        // do something with item
    }
}

private static void Run(Action<int> action, ThreadStart consumer)
{
    new Thread(consumer) { IsBackground = true }.Start();
    Stopwatch stopwatch = Stopwatch.StartNew();
    Parallel.For(0, 100000000, new ParallelOptions() { MaxDegreeOfParallelism = 8 }, action);
    stopwatch.Stop();
    Console.WriteLine(action.Method.Name + " takes " + stopwatch.Elapsed);
}

Ответ 3

Я хотел видеть сравнение производительности для добавления, а также принятия. Вот код, который я использовал:

class Program
{
    static List<int> list = new List<int>();
    static ConcurrentBag<int> bag = new ConcurrentBag<int>();
    static ConcurrentStack<int> stack = new ConcurrentStack<int>();
    static ConcurrentQueue<int> queue = new ConcurrentQueue<int>();
    static void Main(string[] args)
    {
        list = new List<int>();
        run(addList);
        run(takeList);

        list = null;
        GC.Collect();

        bag = new ConcurrentBag<int>();
        run(addBag);
        run(takeBag);

        bag = null;
        GC.Collect();

        stack = new ConcurrentStack<int>();
        run(addStack);
        run(takeStack);

        stack = null;
        GC.Collect();

        queue = new ConcurrentQueue<int>();
        run(addQueue);
        run(takeQueue);

        queue = null;
        GC.Collect();

        Console.ReadLine();
    }

    private static void takeList(int obj)
    {
        lock (list)
        {
            if (list.Count == 0)
                return;

            int output = list[obj];
        }
    }

    private static void takeStack(int obj)
    {
        stack.TryPop(out int output);
    }

    private static void takeQueue(int obj)
    {
        queue.TryDequeue(out int output);
    }

    private static void takeBag(int obj)
    {
        bag.TryTake(out int output);
    }

    private static void addList(int obj) { lock (list) { list.Add(obj); } }

    private static void addStack(int obj) { stack.Push(obj); }

    private static void addQueue(int obj) { queue.Enqueue(obj); }

    private static void addBag(int obj) { bag.Add(obj); }



    private static void run(Action<int> action)
    {
        Stopwatch stopwatch = Stopwatch.StartNew();
        Parallel.For(0, 10000000, new ParallelOptions()
        {
            MaxDegreeOfParallelism = 8
        }, action);
        stopwatch.Stop();
        Console.WriteLine(action.Method.Name + " takes " + stopwatch.Elapsed);
    }
}

И результат:

  • addList принимает 00: 00: 00.8875893
  • takeList принимает 00: 00: 00.7500289
  • addBag принимает 00: 00: 01.8651759
  • takeBag принимает 00: 00: 00.5749322
  • addStack принимает 00: 00: 01.5565545
  • takeStack принимает 00: 00: 00.3838718
  • addQueue принимает 00: 00: 00.8861318
  • takeQueue принимает 00: 00: 01.0510706