Parallel.ForEach и ConcurrentDictionary медленнее, чем обычный словарь

Есть задание. Массив содержит произвольные 9X_c-sharp строки. Нам нужно подсчитать, сколько раз 9X_parallel.foreach каждая из строк встречается в массиве. Решите 9X_threading задачу в одном потоке и многопоточном, сравните 9X_csharp время выполнения.

Почему-то однопоточная 9X_multithreading версия работает быстрее многопоточной: 90 9X_parallel.foreach мс против 300 мс. Как исправить многопоточную 9X_c-sharp версию, чтобы она работала быстрее однопоточной?

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Threading;

namespace ParallelDictionary
{
    class Program
    {
        static void Main(string[] args)
        {
            List strs = new List();
            for (int i=0; i<1000000; i++)
            {
                strs.Add("qqq");
            }
            for (int i=0;i< 5000; i++)
            {
                strs.Add("aaa");
            }

            F(strs);
            ParallelF(strs);
        }

        private static void F(List strs)
        {
            Dictionary freqs = new Dictionary();

            Stopwatch stopwatch = new Stopwatch();
            stopwatch.Start();
            for (int i=0; i strs)
        {
            ConcurrentDictionary freqs = new ConcurrentDictionary();

            Stopwatch stopwatch = new Stopwatch();
            stopwatch.Start();
            Parallel.ForEach(strs, str =>
            {
                freqs.AddOrUpdate(str, 1, (key, value) => value + 1);
            });
            stopwatch.Stop();

            Console.WriteLine("multi-threaded {0} ms", stopwatch.ElapsedMilliseconds);
            foreach (var kvp in freqs)
            {
                Console.WriteLine("{0} {1}", kvp.Key, kvp.Value);
            }
        }
    }
}

6
0
2
Общее количество ответов: 2

Ответ #1

Ответ на вопрос: Parallel.ForEach и ConcurrentDictionary медленнее, чем обычный словарь

Можно сделать многопоточную версию немного 9X_c#.net быстрее, чем однопоточную, используя разделитель 9X_csharp для разделения данных на фрагменты, которые 9X_c-sharp вы обрабатываете отдельно.

Затем каждый фрагмент 9X_threading может быть обработан в отдельный непараллельный 9X_c#-language словарь без какой-либо блокировки. Наконец, в 9X_thread конце каждого диапазона вы можете обновить 9X_c#-language непараллельный словарь результатов (который 9X_threads вам придется заблокировать).

Примерно так:

private static void ParallelF(List strs)
{
    Dictionary result = new Dictionary();

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

    object locker = new object();

    Parallel.ForEach(Partitioner.Create(0, strs.Count), range => 
    {
        var freqs = new Dictionary();

        for (int i = range.Item1; i < range.Item2; ++i)
        {
            if (!freqs.ContainsKey(strs[i]))
                freqs[strs[i]] = 1;
            else
                freqs[strs[i]]++;
        }

        lock (locker)
        { 
            foreach (var kvp in freqs)
            {
                if (!result.ContainsKey(kvp.Key))
                {
                    result[kvp.Key] = kvp.Value;
                }
                else
                {
                    result[kvp.Key] += kvp.Value;
                }
            }
        }
    });

    stopwatch.Stop();

    Console.WriteLine("multi-threaded {0} ms", stopwatch.ElapsedMilliseconds);
    foreach (var kvp in result)
    {
        Console.WriteLine("{0} {1}", kvp.Key, kvp.Value);
    }
}

В 9X_cross-threading моей системе это дает следующие результаты 9X_multithread (для релизной сборки .NET 6):

single-threaded 50 ms
qqq 1000000
aaa 5000
multi-threaded 26 ms
qqq 1000000
aaa 5000

Это лишь немного 9X_c# быстрее... стоит ли это того, решать вам.

10
0

Ответ #2

Ответ на вопрос: Parallel.ForEach и ConcurrentDictionary медленнее, чем обычный словарь

Вот еще один подход, который имеет сходство 9X_threads с решением Matthew Watson на основе Partitioner, но использует другой 9X_thread API. Он использует расширенную перегрузку 9X_cross-threading Parallel.ForEach, подпись которой показана ниже:

/// 
/// Executes a foreach (For Each in Visual Basic) operation with thread-local data
/// on an System.Collections.IEnumerable in which iterations may run in parallel,
/// loop options can be configured, and the state of the loop can be monitored and
/// manipulated.
/// 
public static ParallelLoopResult ForEach(
    IEnumerable source,
    ParallelOptions parallelOptions,
    Func localInit,
    Func body,
    Action localFinally);

В качестве 9X_c#-language TLocal используется локальный словарь, содержащий 9X_concurrentdictionary частичные результаты, вычисленные одним 9X_multithreading рабочим потоком:

static Dictionary GetFrequencies(List source)
{
    Dictionary frequencies = new();
    ParallelOptions options = new()
    {
        MaxDegreeOfParallelism = Environment.ProcessorCount
    };
    Parallel.ForEach(source, options, () => new Dictionary(),
        (item, state, partialFrequencies) =>
    {
        ref int occurences = ref CollectionsMarshal.GetValueRefOrAddDefault(
            partialFrequencies, item, out bool exists);
        occurences++;
        return partialFrequencies;
    }, partialFrequencies =>
    {
        lock (frequencies)
        {
            foreach ((string item, int partialOccurences) in partialFrequencies)
            {
                ref int occurences = ref CollectionsMarshal.GetValueRefOrAddDefault(
                    frequencies, item, out bool exists);
                occurences += partialOccurences;
            }
        }
    });
    return frequencies;
}

Приведенный выше код также 9X_threading демонстрирует использование низкоуровневого 9X_multithreading CollectionsMarshal.GetValueRefOrAddDefault API, позволяющего искать и обновлять словарь 9X_multithreading с помощью одного хэширования ключа.

Я не 9X_c-sharp измерял (и не тестировал) его, но ожидаю, что 9X_c# он будет медленнее, чем решение Мэтью Уотсона. Причина 9X_c#-language в том, что source перечисляется синхронно. Если 9X_multithreading вы можете справиться со сложностью, вы можете 9X_csharp рассмотреть возможность объединения обоих 9X_csharp подходов для достижения оптимальной производительности.

2
0