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

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

Почему-то однопоточная 9X_thread версия работает быстрее многопоточной: 90 9X_concurrentdictionary мс против 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_thread быстрее, чем однопоточную, используя разделитель 9X_c-sharp для разделения данных на фрагменты, которые 9X_multithreading вы обрабатываете отдельно.

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

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

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_multithreading моей системе это дает следующие результаты 9X_c#.net (для релизной сборки .NET 6):

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

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

10
0

Ответ #2

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

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

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

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

2
0