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);
}
}
}
}
Ответ #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# быстрее... стоит ли это того, решать вам.
Ответ #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 подходов для достижения оптимальной производительности.
-
3
-
3
-
6
-
5
-
6
-
3
-
6
-
2
-
3
-
4
-
5
-
4
-
6
-
5
-
3
-
4
-
4
-
4
-
10
-
6
-
6
-
9
-
3
-
2
-
1
-
4
-
4
-
2
-
1
-
2
-
2
-
1
-
1
-
3
-
10
-
5
-
10
-
7
-
8
-
4
-
10
-
2
-
2
-
7
-
10
-
8
-
20
-
6
-
11
-
9