From 52d4473b341839a64ac2215ffa887d671b95d9a3 Mon Sep 17 00:00:00 2001 From: Vladimir Pecanac Date: Sun, 4 Oct 2026 13:14:33 +0200 Subject: [PATCH] Iterate Through a Dictionary: .NET 10, Keys/Values and remove-while-iterating samples, honest benchmarks - Retarget all three projects to net10.0; BenchmarkDotNet 0.15.8, Microsoft.NET.Test.Sdk 18.10.1, MSTest 4.4.1, coverlet.collector 10.1.0 - Add SubDictionaryUsingKeys, SubDictionaryUsingValues and SubDictionaryRemoveWhileIterating with tests - Replace the parallel output test that could not fail with an order-insensitive assertion - Benchmarks: consume results with BenchmarkDotNet's Consumer instead of dead stores; add the string concatenation benchmark with a per-thread accumulator in the parallel case instead of a racy shared local - File-scoped namespaces, ImplicitUsings on the test project, stray space in an interpolation hole removed --- .../BenchmarkRunner/Benchmark.csproj | 4 +- .../DictionaryConcatenateBenchmark.cs | 65 ++++++ .../DictionaryIterateBenchmark.cs | 81 ++++---- .../BenchmarkRunner/ProgramBenchmark.cs | 11 +- .../IterateThroughDictionary.csproj | 2 +- .../IterateThroughDictionary/Program.cs | 103 ++++++---- .../Tests/DictionaryTest.cs | 187 ++++++++++-------- .../Tests/Tests.csproj | 11 +- 8 files changed, 287 insertions(+), 177 deletions(-) create mode 100644 collections-dictionary/IterateThroughDictionary/BenchmarkRunner/DictionaryConcatenateBenchmark.cs diff --git a/collections-dictionary/IterateThroughDictionary/BenchmarkRunner/Benchmark.csproj b/collections-dictionary/IterateThroughDictionary/BenchmarkRunner/Benchmark.csproj index 7c12f24fb4..b557281d53 100644 --- a/collections-dictionary/IterateThroughDictionary/BenchmarkRunner/Benchmark.csproj +++ b/collections-dictionary/IterateThroughDictionary/BenchmarkRunner/Benchmark.csproj @@ -2,13 +2,13 @@ Exe - net6.0 + net10.0 enable enable - + diff --git a/collections-dictionary/IterateThroughDictionary/BenchmarkRunner/DictionaryConcatenateBenchmark.cs b/collections-dictionary/IterateThroughDictionary/BenchmarkRunner/DictionaryConcatenateBenchmark.cs new file mode 100644 index 0000000000..f9cc515c1d --- /dev/null +++ b/collections-dictionary/IterateThroughDictionary/BenchmarkRunner/DictionaryConcatenateBenchmark.cs @@ -0,0 +1,65 @@ +using BenchmarkDotNet.Attributes; + +namespace Benchmark; + +public class DictionaryConcatenateBenchmark +{ + private Dictionary FillData(int count) + { + var testValues = new Dictionary(); + + for (int i = 0; i < count; i++) + { + testValues.Add(i, "value-" + i); + } + + return testValues; + } + + public IEnumerable SampleData() + { + yield return new object[] { FillData(100), "100" }; + yield return new object[] { FillData(1000), "1000" }; + yield return new object[] { FillData(10000), "10000" }; + } + + [Benchmark] + [ArgumentsSource(nameof(SampleData))] + public string WhenDictionaryUsingForEach(Dictionary dictionaryData, string numberOfItems) + { + var result = string.Empty; + + foreach (var testValue in dictionaryData) + { + result += testValue.Value; + } + + return result; + } + + [Benchmark] + [ArgumentsSource(nameof(SampleData))] + public string WhenDictionaryUsingForLoop(Dictionary dictionaryData, string numberOfItems) + { + var result = string.Empty; + + for (int i = 0; i < dictionaryData.Count; i++) + { + var item = dictionaryData.ElementAt(i); + result += item.Value; + } + + return result; + } + + [Benchmark] + [ArgumentsSource(nameof(SampleData))] + public string WhenDictionaryParallelEnumerable(Dictionary dictionaryData, string numberOfItems) + { + using var partialResults = new ThreadLocal(() => string.Empty, trackAllValues: true); + + dictionaryData.AsParallel().ForAll(testValue => partialResults.Value += testValue.Value); + + return string.Concat(partialResults.Values); + } +} diff --git a/collections-dictionary/IterateThroughDictionary/BenchmarkRunner/DictionaryIterateBenchmark.cs b/collections-dictionary/IterateThroughDictionary/BenchmarkRunner/DictionaryIterateBenchmark.cs index 55f54f8814..3fcb9e3e92 100644 --- a/collections-dictionary/IterateThroughDictionary/BenchmarkRunner/DictionaryIterateBenchmark.cs +++ b/collections-dictionary/IterateThroughDictionary/BenchmarkRunner/DictionaryIterateBenchmark.cs @@ -1,57 +1,56 @@ using BenchmarkDotNet.Attributes; -using BenchmarkDotNet.Order; +using BenchmarkDotNet.Engines; -namespace Benchmark -{ - public class DictionaryIterateBenchmark - { - private Dictionary FillData(int count) - { - var testValues = new Dictionary(); +namespace Benchmark; - for (int i = 0; i < count; i++) - { - testValues.Add(i, "value-" + i); - } +public class DictionaryIterateBenchmark +{ + private readonly Consumer _consumer = new(); - return testValues; - } + private Dictionary FillData(int count) + { + var testValues = new Dictionary(); - public IEnumerable SampleData() + for (int i = 0; i < count; i++) { - yield return new object[] { FillData(100), "100" }; - yield return new object[] { FillData(1000), "1000" }; - yield return new object[] { FillData(10000), "10000" }; + testValues.Add(i, "value-" + i); } - [Benchmark] - [ArgumentsSource(nameof(SampleData))] - public void WhenDictionaryUsingForEach(Dictionary dictionaryData, string numberOfItems) - { - foreach (var testValue in dictionaryData) - { - var result = testValue.Value; - } - } + return testValues; + } - [Benchmark] - [ArgumentsSource(nameof(SampleData))] - public void WhenDictionaryUsingForLoop(Dictionary dictionaryData, string numberOfItems) + public IEnumerable SampleData() + { + yield return new object[] { FillData(100), "100" }; + yield return new object[] { FillData(1000), "1000" }; + yield return new object[] { FillData(10000), "10000" }; + } + + [Benchmark] + [ArgumentsSource(nameof(SampleData))] + public void WhenDictionaryUsingForEach(Dictionary dictionaryData, string numberOfItems) + { + foreach (var testValue in dictionaryData) { - for (int i = 0; i < dictionaryData.Count; i++) - { - var item = dictionaryData.ElementAt(i); - var result = item.Value; - } + _consumer.Consume(testValue.Value); } + } - [Benchmark] - [ArgumentsSource(nameof(SampleData))] - public void WhenDictionaryParallelEnumerable(Dictionary dictionaryData, string numberOfItems) + [Benchmark] + [ArgumentsSource(nameof(SampleData))] + public void WhenDictionaryUsingForLoop(Dictionary dictionaryData, string numberOfItems) + { + for (int i = 0; i < dictionaryData.Count; i++) { - var result = string.Empty; - - dictionaryData.AsParallel().ForAll(testValue => result = testValue.Value); + var item = dictionaryData.ElementAt(i); + _consumer.Consume(item.Value); } } + + [Benchmark] + [ArgumentsSource(nameof(SampleData))] + public void WhenDictionaryParallelEnumerable(Dictionary dictionaryData, string numberOfItems) + { + dictionaryData.AsParallel().ForAll(testValue => _consumer.Consume(testValue.Value)); + } } diff --git a/collections-dictionary/IterateThroughDictionary/BenchmarkRunner/ProgramBenchmark.cs b/collections-dictionary/IterateThroughDictionary/BenchmarkRunner/ProgramBenchmark.cs index 8c9105e909..4c51cd8656 100644 --- a/collections-dictionary/IterateThroughDictionary/BenchmarkRunner/ProgramBenchmark.cs +++ b/collections-dictionary/IterateThroughDictionary/BenchmarkRunner/ProgramBenchmark.cs @@ -1,12 +1,11 @@ using BenchmarkDotNet.Running; -namespace Benchmark +namespace Benchmark; + +public class ProgramBenchmark { - public class ProgramBenchmark + static void Main(string[] args) { - static void Main(string[] args) - { - BenchmarkRunner.Run(); - } + BenchmarkRunner.Run(typeof(ProgramBenchmark).Assembly, args: args); } } diff --git a/collections-dictionary/IterateThroughDictionary/IterateThroughDictionary/IterateThroughDictionary.csproj b/collections-dictionary/IterateThroughDictionary/IterateThroughDictionary/IterateThroughDictionary.csproj index 40c60dd4c8..7f1dc2eb2f 100644 --- a/collections-dictionary/IterateThroughDictionary/IterateThroughDictionary/IterateThroughDictionary.csproj +++ b/collections-dictionary/IterateThroughDictionary/IterateThroughDictionary/IterateThroughDictionary.csproj @@ -2,7 +2,7 @@ Exe - net6.0 + net10.0 enable enable diff --git a/collections-dictionary/IterateThroughDictionary/IterateThroughDictionary/Program.cs b/collections-dictionary/IterateThroughDictionary/IterateThroughDictionary/Program.cs index cfbba7f557..957b4830f6 100644 --- a/collections-dictionary/IterateThroughDictionary/IterateThroughDictionary/Program.cs +++ b/collections-dictionary/IterateThroughDictionary/IterateThroughDictionary/Program.cs @@ -1,58 +1,87 @@ -namespace IterateThroughDictionary +namespace IterateThroughDictionary; + +public class Program { - public class Program + public static void Main(string[] args) { - public static void Main(string[] args) + var monthsInYear = new Dictionary { - var monthsInYear = new Dictionary - { - {1, "January" }, - {2, "February" }, - {3, "March" }, - {4, "April" } - }; - - SubDictionaryUsingForEach(monthsInYear); - SubDictionaryKeyValuePair(monthsInYear); - SubDictionaryForLoop(monthsInYear); - SubDictionaryParallelEnumerable(monthsInYear); + {1, "January" }, + {2, "February" }, + {3, "March" }, + {4, "April" } + }; + + SubDictionaryUsingForEach(monthsInYear); + SubDictionaryKeyValuePair(monthsInYear); + SubDictionaryUsingKeys(monthsInYear); + SubDictionaryUsingValues(monthsInYear); + SubDictionaryForLoop(monthsInYear); + SubDictionaryParallelEnumerable(monthsInYear); + SubDictionaryRemoveWhileIterating(monthsInYear); + } + + public static void SubDictionaryUsingForEach(Dictionary monthsInYear) + { + foreach (var month in monthsInYear) + { + Console.WriteLine($"{month.Key} : {month.Value}"); } + } - public static void SubDictionaryUsingForEach(Dictionary monthsInYear) + public static void SubDictionaryKeyValuePair(Dictionary monthsInYear) + { + foreach (KeyValuePair entry in monthsInYear) { - foreach (var month in monthsInYear) - { - Console.WriteLine($"{month.Key } : {month.Value}"); - } + Console.WriteLine($"{entry.Key} : {entry.Value}"); } - public static void SubDictionaryKeyValuePair(Dictionary monthsInYear) + foreach (var (key, value) in monthsInYear) { - foreach (KeyValuePair entry in monthsInYear) - { - Console.WriteLine($"{entry.Key} : {entry.Value}"); - } + Console.WriteLine($"{key} : {value}"); + } + } - foreach (var (key, value) in monthsInYear) - { - Console.WriteLine($"{key} : {value}"); - } + public static void SubDictionaryUsingKeys(Dictionary monthsInYear) + { + foreach (var monthNumber in monthsInYear.Keys) + { + Console.WriteLine(monthNumber); } + } - public static void SubDictionaryForLoop(Dictionary monthsInYear) + public static void SubDictionaryUsingValues(Dictionary monthsInYear) + { + foreach (var monthName in monthsInYear.Values) { - for (int index = 0; index < monthsInYear.Count; index++) - { - KeyValuePair month = monthsInYear.ElementAt(index); + Console.WriteLine(monthName); + } + } - Console.WriteLine($"{month.Key} : {month.Value}"); + public static void SubDictionaryRemoveWhileIterating(Dictionary monthsInYear) + { + foreach (var (monthNumber, monthName) in monthsInYear) + { + if (monthName.StartsWith('J')) + { + monthsInYear.Remove(monthNumber); } } + } - public static void SubDictionaryParallelEnumerable(Dictionary monthsInYear) + public static void SubDictionaryForLoop(Dictionary monthsInYear) + { + for (int index = 0; index < monthsInYear.Count; index++) { - monthsInYear.AsParallel() - .ForAll(month => Console.WriteLine($"{month.Key} : {month.Value}")); + KeyValuePair month = monthsInYear.ElementAt(index); + + Console.WriteLine($"{month.Key} : {month.Value}"); } } + + public static void SubDictionaryParallelEnumerable(Dictionary monthsInYear) + { + monthsInYear.AsParallel() + .ForAll(month => Console.WriteLine($"{month.Key} : {month.Value}")); + } } diff --git a/collections-dictionary/IterateThroughDictionary/Tests/DictionaryTest.cs b/collections-dictionary/IterateThroughDictionary/Tests/DictionaryTest.cs index 9900f8dfe5..38d095ceda 100644 --- a/collections-dictionary/IterateThroughDictionary/Tests/DictionaryTest.cs +++ b/collections-dictionary/IterateThroughDictionary/Tests/DictionaryTest.cs @@ -1,92 +1,109 @@ -using System; using Microsoft.VisualStudio.TestTools.UnitTesting; using IterateThroughDictionary; -using System.Collections.Generic; -using System.IO; -namespace DictionaryTests +namespace DictionaryTests; + +[TestClass] +public class DictionaryTest { - [TestClass] - public class DictionaryTest + public static readonly string _monthJanuary = "1 : January"; + public static readonly string _monthFebruary = "2 : February"; + public static readonly string _monthMarch = "3 : March"; + public static readonly string _monthApril = "4 : April"; + public static readonly string _monthJanuaryStringJoin = "[1, January]"; + + private StringWriter _stringWriter = new StringWriter(); + + public DictionaryTest() + { + Console.SetOut(_stringWriter); + } + + private static Dictionary _months = new Dictionary + { + {1,"January" }, + {2,"February" }, + {3,"March" }, + {4,"April" } + }; + + [TestMethod] + public void WhenDictionaryUsesForEach_ThenOutputsReqdResults() + { + Program.SubDictionaryUsingForEach(_months); + + var outputLines = _stringWriter.ToString().Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries); + + Assert.AreEqual(_monthJanuary, outputLines[0]); + Assert.AreEqual(_monthFebruary, outputLines[1]); + Assert.AreEqual(_monthMarch, outputLines[2]); + Assert.AreEqual(_monthApril, outputLines[3]); + } + + [TestMethod] + public void WhenDictionaryUsesKeyValuePair_ThenOutputsReqdResults() + { + Program.SubDictionaryKeyValuePair(_months); + + var outputLines = _stringWriter.ToString().Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries); + + Assert.AreEqual(_monthJanuary, outputLines[0]); + Assert.AreEqual(_monthFebruary, outputLines[1]); + Assert.AreEqual(_monthMarch, outputLines[2]); + Assert.AreEqual(_monthApril, outputLines[3]); + } + + [TestMethod] + public void WhenDictionaryUsesKeys_ThenOutputsEveryKey() { - public static readonly string _monthJanuary = "1 : January"; - public static readonly string _monthFebruary = "2 : February"; - public static readonly string _monthMarch = "3 : March"; - public static readonly string _monthApril = "4 : April"; - public static readonly string _monthJanuaryStringJoin = "[1, January]"; - - private StringWriter _stringWriter = new StringWriter(); - - public DictionaryTest() - { - Console.SetOut(_stringWriter); - } - - private static Dictionary _months = new Dictionary - { - {1,"January" }, - {2,"February" }, - {3,"March" }, - {4,"April" } - }; - - [TestMethod] - public void WhenDictionaryUsesForEach_ThenOutputsReqdResults() - { - Program.SubDictionaryUsingForEach(_months); - - var outputLines = _stringWriter.ToString().Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries); - - Assert.AreEqual(_monthJanuary, outputLines[0]); - Assert.AreEqual(_monthFebruary, outputLines[1]); - Assert.AreEqual(_monthMarch, outputLines[2]); - Assert.AreEqual(_monthApril, outputLines[3]); - } - - [TestMethod] - public void WhenDictionaryUsesKeyValuePair_ThenOutputsReqdResults() - { - Program.SubDictionaryKeyValuePair(_months); - - var outputLines = _stringWriter.ToString().Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries); - - Assert.AreEqual(_monthJanuary, outputLines[0]); - Assert.AreEqual(_monthFebruary, outputLines[1]); - Assert.AreEqual(_monthMarch, outputLines[2]); - Assert.AreEqual(_monthApril, outputLines[3]); - } - - [TestMethod] - public void WhenDictionaryUsesForLoop_ThenOutputsReqdResults() - { - Program.SubDictionaryForLoop(_months); - - var outputLines = _stringWriter.ToString().Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries); - - Assert.AreEqual(_monthJanuary, outputLines[0]); - Assert.AreEqual(_monthFebruary, outputLines[1]); - Assert.AreEqual(_monthMarch, outputLines[2]); - Assert.AreEqual(_monthApril, outputLines[3]); - } - - [TestMethod] - public void WhenDictionaryUsesParallelEnumerable_ThenOutputsReqdResults() - { - Program.SubDictionaryParallelEnumerable(_months); - - var resultlines = _stringWriter.ToString().Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries); - - for (int i = 0; i <= resultlines.Length; i++) - { - if (resultlines[0].ToString() == _monthJanuary) - Assert.AreEqual(_monthJanuary, resultlines[0]); - if (resultlines[1].ToString() == _monthFebruary) - Assert.AreEqual(_monthFebruary, resultlines[1]); - if (resultlines[2].ToString() == _monthMarch) - Assert.AreEqual(_monthMarch, resultlines[2]); - if (resultlines[3].ToString() == _monthApril) - Assert.AreEqual(_monthApril, resultlines[3]); - } - } + Program.SubDictionaryUsingKeys(_months); + + var outputLines = _stringWriter.ToString().Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries); + + CollectionAssert.AreEqual(new[] { "1", "2", "3", "4" }, outputLines); + } + + [TestMethod] + public void WhenDictionaryUsesValues_ThenOutputsEveryValue() + { + Program.SubDictionaryUsingValues(_months); + + var outputLines = _stringWriter.ToString().Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries); + + CollectionAssert.AreEqual(new[] { "January", "February", "March", "April" }, outputLines); + } + + [TestMethod] + public void WhenDictionaryRemovesWhileIterating_ThenOnlyMatchingEntriesAreRemoved() + { + var months = new Dictionary(_months); + + Program.SubDictionaryRemoveWhileIterating(months); + + CollectionAssert.AreEquivalent(new[] { 2, 3, 4 }, months.Keys.ToArray()); + CollectionAssert.AreEquivalent(new[] { "February", "March", "April" }, months.Values.ToArray()); + } + + [TestMethod] + public void WhenDictionaryUsesForLoop_ThenOutputsReqdResults() + { + Program.SubDictionaryForLoop(_months); + + var outputLines = _stringWriter.ToString().Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries); + + Assert.AreEqual(_monthJanuary, outputLines[0]); + Assert.AreEqual(_monthFebruary, outputLines[1]); + Assert.AreEqual(_monthMarch, outputLines[2]); + Assert.AreEqual(_monthApril, outputLines[3]); + } + + [TestMethod] + public void WhenDictionaryUsesParallelEnumerable_ThenOutputsReqdResults() + { + Program.SubDictionaryParallelEnumerable(_months); + + var resultlines = _stringWriter.ToString().Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries); + + CollectionAssert.AreEquivalent(new[] { _monthJanuary, _monthFebruary, _monthMarch, _monthApril }, resultlines); } } diff --git a/collections-dictionary/IterateThroughDictionary/Tests/Tests.csproj b/collections-dictionary/IterateThroughDictionary/Tests/Tests.csproj index a240ee0b79..f1d5ce0bde 100644 --- a/collections-dictionary/IterateThroughDictionary/Tests/Tests.csproj +++ b/collections-dictionary/IterateThroughDictionary/Tests/Tests.csproj @@ -1,17 +1,18 @@ - net6.0 + net10.0 + enable enable false - - - - + + + +