Created
March 12, 2017 13:53
-
-
Save bdrupieski/603acb37b8751483e48e9e43c437f7cd to your computer and use it in GitHub Desktop.
Performance and correctness comparison between foreach and Parallel.ForEach for adding items to a generic Dictionary<TKey, TValue>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections.Generic; | |
using System.Diagnostics; | |
using System.Linq; | |
using System.Threading.Tasks; | |
namespace App | |
{ | |
/// <summary> | |
/// This snippet shows a performance and correctness comparison between | |
/// foreach and <see cref="Parallel.ForEach"/> when adding a small number | |
/// of objects to a <see cref="Dictionary{TKey,TValue}"/>. | |
/// | |
/// For a few items using a simple foreach loop is much faster and | |
/// using Parallel.ForEach is much slower. Adding items to | |
/// <see cref="Dictionary{TKey,TValue}"/> is also not thread-safe, | |
/// so using <see cref="Parallel.ForEach"/> sometimes results in | |
/// not adding all of the items you intended to add. | |
/// | |
/// It can also throw an exception in <see cref="Dictionary{TKey,TValue}.Add"/> | |
/// and get stuck in an infinite loop I think in | |
/// <see cref="Dictionary{TKey,TValue}"/>'s private FindEntry method. | |
/// | |
/// Note that you'll have to try to run this multiple times for it to actually | |
/// complete because <see cref="ParallelForEach"/> will sometimes enter | |
/// an infinite loop. | |
/// </summary> | |
public class CompareForEachToParallelForEachForAddingToDictionary | |
{ | |
private const int Runs = 100000; | |
private static readonly Dictionary<string, object> ActionArguments = new Dictionary<string, object> | |
{ | |
{"enrollmentId", 17L}, | |
{"userid", "domain//username"}, | |
{"data", null} | |
}; | |
public static void DoIt() | |
{ | |
var forEachMilliseconds = ForEach(); | |
var parallelForEachMilliseconds = ParallelForEach(); | |
var forEachTimesFasterThanParallel = (double)parallelForEachMilliseconds / forEachMilliseconds; | |
Console.WriteLine($"{nameof(ForEach)} is {forEachTimesFasterThanParallel:0.#}x faster than {nameof(ParallelForEach)}"); | |
Console.WriteLine(); | |
Console.WriteLine("Bonus:"); | |
var linqMilliseconds = LinqToDictionary(); | |
var forEachTimesFasterThanLinq = (double)linqMilliseconds / forEachMilliseconds; | |
Console.WriteLine($"{nameof(ForEach)} is {forEachTimesFasterThanLinq:0.#}x faster than {nameof(LinqToDictionary)}"); | |
} | |
private static long ParallelForEach() | |
{ | |
int numTimesFailedToAddAllItems = 0; | |
int numTimesThrewAnException = 0; | |
var sw = Stopwatch.StartNew(); | |
for (int i = 0; i < Runs; i++) | |
{ | |
try | |
{ | |
var arguments = new Dictionary<string, string>(); | |
Parallel.ForEach(ActionArguments, item => | |
{ | |
string value = item.Value?.ToString() ?? "<null>"; | |
arguments.Add(item.Key, value); | |
}); | |
if (arguments.Count != ActionArguments.Count) | |
{ | |
numTimesFailedToAddAllItems++; | |
} | |
} | |
catch (Exception) | |
{ | |
numTimesThrewAnException++; | |
} | |
} | |
sw.Stop(); | |
Console.WriteLine($"{sw.ElapsedMilliseconds} ms using {nameof(ParallelForEach)}."); | |
Console.WriteLine($"{nameof(ParallelForEach)} failed to add all items {numTimesFailedToAddAllItems} times."); | |
Console.WriteLine($"{nameof(ParallelForEach)} threw an exception {numTimesThrewAnException} times."); | |
Console.WriteLine(); | |
return sw.ElapsedMilliseconds; | |
} | |
private static long ForEach() | |
{ | |
int numTimesFailedToAddAllItems = 0; | |
var sw = Stopwatch.StartNew(); | |
for (int i = 0; i < Runs; i++) | |
{ | |
var arguments = new Dictionary<string, string>(); | |
foreach (var actionArgument in ActionArguments) | |
{ | |
string value = actionArgument.Value?.ToString() ?? "<null>"; | |
arguments.Add(actionArgument.Key, value); | |
} | |
if (arguments.Count != ActionArguments.Count) | |
{ | |
numTimesFailedToAddAllItems++; | |
} | |
} | |
sw.Stop(); | |
Console.WriteLine($"{sw.ElapsedMilliseconds} ms using {nameof(ForEach)}."); | |
Console.WriteLine($"{nameof(ForEach)} failed to add all items {numTimesFailedToAddAllItems} times."); | |
Console.WriteLine(); | |
return sw.ElapsedMilliseconds; | |
} | |
private static long LinqToDictionary() | |
{ | |
int numTimesFailedToAddAllItems = 0; | |
var sw = Stopwatch.StartNew(); | |
for (int i = 0; i < Runs; i++) | |
{ | |
var arguments = ActionArguments.ToDictionary(x => x.Key, y => new KeyValuePair<string, string>(y.Key, y.Value?.ToString() ?? "<null>")); | |
if (arguments.Count != ActionArguments.Count) | |
{ | |
numTimesFailedToAddAllItems++; | |
} | |
} | |
sw.Stop(); | |
Console.WriteLine($"{sw.ElapsedMilliseconds} ms using {nameof(LinqToDictionary)}."); | |
Console.WriteLine($"{nameof(LinqToDictionary)} failed to add all items {numTimesFailedToAddAllItems} times."); | |
Console.WriteLine(); | |
return sw.ElapsedMilliseconds; | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment