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

Есть задание. Массив содержит произвольные 9X_threads строки. Нам нужно подсчитать, сколько раз 9X_multithreading каждая из строк встречается в массиве. Решите 9X_cross-threading задачу в одном потоке и многопоточном, сравните 9X_c#.net время выполнения.

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

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_cross-threading быстрее, чем однопоточную, используя разделитель 9X_c# для разделения данных на фрагменты, которые 9X_cross-threading вы обрабатываете отдельно.

Затем каждый фрагмент 9X_cross-threading может быть обработан в отдельный непараллельный 9X_threads словарь без какой-либо блокировки. Наконец, в 9X_parallel.foreach конце каждого диапазона вы можете обновить 9X_multithreading непараллельный словарь результатов (который 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_c#.net (для релизной сборки .NET 6):

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

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

10
0

Ответ #2

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

Вот еще один подход, который имеет сходство 9X_visual-c# с решением Matthew Watson на основе Partitioner, но использует другой 9X_cross-threading API. Он использует расширенную перегрузку 9X_c-sharp 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# TLocal используется локальный словарь, содержащий 9X_c# частичные результаты, вычисленные одним 9X_multithread рабочим потоком:

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_parallel.foreach демонстрирует использование низкоуровневого 9X_multithread CollectionsMarshal.GetValueRefOrAddDefault API, позволяющего искать и обновлять словарь 9X_c#-language с помощью одного хэширования ключа.

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

2
0