Почему ConcurrentBag <T>так медленно работает в .Net (4.0)? Я делаю это неправильно? - PullRequest
42 голосов
/ 24 января 2011

Перед тем, как начать проект, я написал простой тест для сравнения производительности ConcurrentBag из (System.Collections.Concurrent) относительно блокировки и списков.Я чрезвычайно удивлен, что ConcurrentBag более чем в 10 раз медленнее, чем блокировка с помощью простого List.Из того, что я понимаю, ConcurrentBag работает лучше всего, когда читатель и писатель находятся в одном потоке.Тем не менее, я не думал, что его производительность будет намного хуже, чем у традиционных блокировок.

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

private static void ConcurrentBagTest()
   {
        int collSize = 10000000;
        Stopwatch stopWatch = new Stopwatch();
        ConcurrentBag<int> bag1 = new ConcurrentBag<int>();

        stopWatch.Start();


        Parallel.For(0, collSize, delegate(int i)
        {
            bag1.Add(i);
        });


        stopWatch.Stop();
        Console.WriteLine("Elapsed Time = {0}", 
                          stopWatch.Elapsed.TotalSeconds);
 }

На моем компьютере это занимает 3-4 секунды, по сравнению с 0,5 - 0,9 секундами этого кода:

       private static void LockCollTest()
       {
        int collSize = 10000000;
        object list1_lock=new object();
        List<int> lst1 = new List<int>(collSize);

        Stopwatch stopWatch = new Stopwatch();
        stopWatch.Start();


        Parallel.For(0, collSize, delegate(int i)
            {
                lock(list1_lock)
                {
                    lst1.Add(i);
                }
            });

        stopWatch.Stop();
        Console.WriteLine("Elapsed = {0}", 
                          stopWatch.Elapsed.TotalSeconds);
       }

Как я уже говорил, одновременное чтение и запись не помогают при одновременном тестировании пакета.Я делаю что-то не так или эта структура данных просто очень медленная?

[РЕДАКТИРОВАТЬ] - Я удалил Задачи, потому что они мне здесь не нужны (полный код читал другую задачу)

[Редактировать] Большое спасибо за ответы.Я с трудом выбираю «правильный ответ», так как кажется, что это сочетание нескольких ответов.

Как отметил Майкл Гольдштейн, скорость действительно зависит от данных.Дарин отметил, что для того, чтобы ConcurrentBag работал быстрее, должно быть больше конфликтов, и Parallel.For не обязательно запускает одинаковое количество потоков.Один момент, который нужно отнять, - не делать ничего, что вам не нужно внутри замка 1016 *.В приведенном выше случае я не вижу, что я делаю что-либо внутри блокировки, за исключением того, что я могу присвоить значение переменной temp.

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

Я провел несколько тестов, запустив 15 задач, и результаты зависели, помимо прочего, от размера коллекции.Однако ConcurrentBag работал почти так же, как и лучше, чем блокировка списка, до 1 миллиона вставок.При превышении 1 миллиона блокировок иногда казались намного быстрее, но, вероятно, у меня никогда не будет большей структуры данных для моего проекта.Вот код, который я запустил:

        int collSize = 1000000;
        object list1_lock=new object();
        List<int> lst1 = new List<int>();
        ConcurrentBag<int> concBag = new ConcurrentBag<int>();
        int numTasks = 15;

        int i = 0;

        Stopwatch sWatch = new Stopwatch();
        sWatch.Start();
         //First, try locks
        Task.WaitAll(Enumerable.Range(1, numTasks)
           .Select(x => Task.Factory.StartNew(() =>
            {
                for (i = 0; i < collSize / numTasks; i++)
                {
                    lock (list1_lock)
                    {
                        lst1.Add(x);
                    }
                }
            })).ToArray());

        sWatch.Stop();
        Console.WriteLine("lock test. Elapsed = {0}", 
            sWatch.Elapsed.TotalSeconds);

        // now try concurrentBag
        sWatch.Restart();
        Task.WaitAll(Enumerable.Range(1, numTasks).
                Select(x => Task.Factory.StartNew(() =>
            {
                for (i = 0; i < collSize / numTasks; i++)
                {
                    concBag.Add(x);
                }
            })).ToArray());

        sWatch.Stop();
        Console.WriteLine("Conc Bag test. Elapsed = {0}",
               sWatch.Elapsed.TotalSeconds);

Ответы [ 11 ]

43 голосов
/ 24 января 2011

Позвольте мне спросить вас: насколько реально, что у вас есть приложение, которое постоянно добавляет в коллекцию и никогда не читает из нее ?Какая польза от такой коллекции?(Это не чисто риторический вопрос. Я мог бы представить, что существуют случаи использования, когда, например, вы читаете только из коллекции при завершении работы (для регистрации) или по запросу пользователя. Я считаю, что эти сценарии довольноправда, редко.)

Это то, что симулирует ваш код.Вызов List<T>.Add будет молниеносным во всех случаях, кроме случая, когда список должен изменить размер своего внутреннего массива;но это сглаживается всеми другими добавлениями, которые происходят довольно быстро.Таким образом, вы вряд ли увидите значительное количество разногласий в этом контексте, особенно тестирование на персональном ПК, например, даже с 8 ядрами (как вы заявили, где-то в комментарии). Может быть, вы можете увидеть больше конфликтов на чем-то вроде 24-ядерного компьютера, где многие ядра могут пытаться добавить в список буквально одновременно.

Скорее всего, разногласия начнутся там, где вы прочитаете из своей коллекции, особеннов foreach циклах (или запросах LINQ, которые составляют foreach циклы под капотом), которые требуют блокировки всей операции, чтобы вы не изменяли свою коллекцию при ее повторении.

Если вы можете реальноВоспроизведите этот сценарий, я думаю, вы увидите ConcurrentBag<T> масштаб намного лучше, чем показывает ваш текущий тест.


Обновление : Здесь - это программа, которую янаписал, чтобы сравнить эти коллекции в сценарии, который я описал выше (много авторов, много читателей).Запустив 25 испытаний с размером коллекции 10000 и 8 нитями считывателя, я получил следующие результаты:

Took 529.0095 ms to add 10000 elements to a List<double> with 8 reader threads.
Took 39.5237 ms to add 10000 elements to a ConcurrentBag<double> with 8 reader threads.
Took 309.4475 ms to add 10000 elements to a List<double> with 8 reader threads.
Took 81.1967 ms to add 10000 elements to a ConcurrentBag<double> with 8 reader threads.
Took 228.7669 ms to add 10000 elements to a List<double> with 8 reader threads.
Took 164.8376 ms to add 10000 elements to a ConcurrentBag<double> with 8 reader threads.
[ ... ]
<b>Average list time: 176.072456 ms.</b>
<b>Average bag time: 59.603656 ms.</b>

Очевидно, это зависит от того, что именно вы делаете с этими коллекциями.

15 голосов
/ 07 июня 2012

Кажется, в .NET Framework 4 есть ошибка, которую Microsoft исправила в 4.5, похоже, они не ожидали, что ConcurrentBag будет использоваться часто.

См. Следующий пост Ayende для получения дополнительной информации

http://ayende.com/blog/156097/the-high-cost-of-concurrentbag-in-net-4-0

9 голосов
/ 24 января 2011

Глядя на программу, используя визуализатор конкуренции MS, видно, что ConcurrentBag<T> имеет гораздо более высокую стоимость, связанную с параллельной вставкой, чем просто блокировка на List<T>.Одна вещь, на которую я обратил внимание, это то, что затраты на вращение 6 потоков (используемых на моей машине), по-видимому, связаны с началом первого ConcurrentBag<T> запуска (холодный запуск).Затем используется 5 или 6 потоков с кодом List<T>, который быстрее (теплый прогон).Добавление еще одного ConcurrentBag<T> прогона после того, как список показывает, что оно занимает меньше времени, чем первый («горячий прогон»).

Из того, что я вижу в споре, в реализации ConcurrentBag<T> много времени уходитвыделяя память.Удаление явного выделения размера из кода List<T> замедляет его, но не настолько, чтобы что-то изменить.

EDIT: похоже, что ConcurrentBag<T> внутренне сохраняетlist per Thread.CurrentThread, блокирует 2-4 раза в зависимости от того, запущен ли он в новом потоке, и выполняет хотя бы один Interlocked.Exchange.Как отмечалось в MSDN: «оптимизировано для сценариев, в которых один и тот же поток будет производить и потреблять данные, хранящиеся в пакете».Это наиболее вероятное объяснение снижения производительности по сравнению с необработанным списком.

9 голосов
/ 24 января 2011

Как общий ответ:

  • Параллельные коллекции, которые используют блокировку, могут быть очень быстрыми, если для их данных практически нет конкуренции (т.е. блокировок). Это связано с тем, что такие классы коллекций часто создаются с использованием очень недорогих блокирующих примитивов, особенно когда они не удовлетворены.
  • Коллекции без блокировок могут быть медленнее из-за уловок, используемых для предотвращения блокировок, а также из-за других узких мест, таких как ложное совместное использование, сложность, необходимая для реализации их природы без блокировки, приводящей к ошибкам кэша и т. Д.

Подводя итог, можно сказать, что решение о том, какой путь быстрее, в значительной степени зависит от используемых структур данных и количества конфликтов за блокировки среди других проблем (например, число считывателей и записывающих в общей / исключительной договоренности типов).

Ваш конкретный пример имеет очень высокую степень раздора, поэтому я должен сказать, что я удивлен поведением. С другой стороны, объем работы, выполняемой при сохранении блокировки, очень мал, так что, возможно, в конце концов, для самой блокировки будет мало споров. Также могут быть недостатки в реализации обработки параллелизма ConcurrentBag, что делает ваш конкретный пример (с частыми вставками и без чтения) плохим вариантом его использования.

5 голосов
/ 04 сентября 2012

Это уже решено в .NET 4.5. Основная проблема заключалась в том, что ThreadLocal, который использует ConcurrentBag, не ожидал иметь много экземпляров. Это было исправлено и теперь может работать довольно быстро.

source - ВЫСОКАЯ стоимость ConcurrentBag в .NET 4.0

3 голосов
/ 24 января 2011

Как сказал @ Darin-Dimitrov, я подозреваю, что ваш Parallel.For на самом деле не порождает одинаковое количество потоков в каждом из двух результатов.Попробуйте вручную создать N потоков, чтобы убедиться, что в обоих случаях вы действительно видите конфликт потоков.

1 голос
/ 25 января 2011

Я предполагаю, что замки не испытывают большого раздора. Я бы порекомендовал прочитать следующую статью: Теория и практика Java: анатомия ошибочного микробенчмарка . В статье обсуждается блокировка микробенчмарка. Как указано в статье, в таких ситуациях необходимо учитывать множество факторов.

1 голос
/ 24 января 2011

Обычно у вас очень мало одновременных записей и нет конфликтов (Parallel.For не обязательно означает много потоков).Попробуйте распараллелить записи, и вы увидите разные результаты:

class Program
{
    private static object list1_lock = new object();
    private const int collSize = 1000;

    static void Main()
    {
        ConcurrentBagTest();
        LockCollTest();
    }

    private static void ConcurrentBagTest()
    {
        var bag1 = new ConcurrentBag<int>();
        var stopWatch = Stopwatch.StartNew();
        Task.WaitAll(Enumerable.Range(1, collSize).Select(x => Task.Factory.StartNew(() =>
        {
            Thread.Sleep(5);
            bag1.Add(x);
        })).ToArray());
        stopWatch.Stop();
        Console.WriteLine("Elapsed Time = {0}", stopWatch.Elapsed.TotalSeconds);
    }

    private static void LockCollTest()
    {
        var lst1 = new List<int>(collSize);
        var stopWatch = Stopwatch.StartNew();
        Task.WaitAll(Enumerable.Range(1, collSize).Select(x => Task.Factory.StartNew(() =>
        {
            lock (list1_lock)
            {
                Thread.Sleep(5);
                lst1.Add(x);
            }
        })).ToArray());
        stopWatch.Stop();
        Console.WriteLine("Elapsed = {0}", stopWatch.Elapsed.TotalSeconds);
    }
}
0 голосов
/ 02 апреля 2011

Похоже, что ConcurrentBag просто медленнее, чем другие параллельные коллекции.

Я думаю, что это проблема реализации - ANTS Profiler показывает, что он застрял в нескольких местах - включая копию массива.

Использование параллельного словаря в тысячи раз быстрее.

0 голосов
/ 24 февраля 2011

Поскольку тело цикла небольшое, вы можете попробовать использовать метод Create для класса Partitioner ...

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

Как: ускорить работу небольших петлевых тел

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...