From b46a71e1baf12d899beb3570669970f217d5d35c Mon Sep 17 00:00:00 2001 From: Kyle Hickman Date: Thu, 13 Nov 2025 15:04:17 -0600 Subject: [PATCH 1/2] test: add more test cases --- .../AdvancedUsage/PatternMatchingTests.cs | 15 ++++ .../BasicUsage/EntryTests.cs | 74 ++++++++++++++++++- 2 files changed, 87 insertions(+), 2 deletions(-) diff --git a/src/DictionaryEntry.Tests/AdvancedUsage/PatternMatchingTests.cs b/src/DictionaryEntry.Tests/AdvancedUsage/PatternMatchingTests.cs index e95c0b4..32daaad 100644 --- a/src/DictionaryEntry.Tests/AdvancedUsage/PatternMatchingTests.cs +++ b/src/DictionaryEntry.Tests/AdvancedUsage/PatternMatchingTests.cs @@ -38,6 +38,21 @@ public void Match_WithVacantEntry_CallsVacantAction() Assert.True(vacantCalled); } + [Fact] + public void Match_WithVacantEntry_CanInsertThroughVacantAction() + { + // Arrange + var dict = new Dictionary(); + + // Act + dict.Entry("key").Match( + occupied => { }, + vacant => vacant.Insert(10)); + + // Assert + Assert.Equal(10, dict["key"]); + } + [Fact] public void Match_WithFuncReturnsCorrectValue() { diff --git a/src/DictionaryEntry.Tests/BasicUsage/EntryTests.cs b/src/DictionaryEntry.Tests/BasicUsage/EntryTests.cs index 0238ba0..52e6a0c 100644 --- a/src/DictionaryEntry.Tests/BasicUsage/EntryTests.cs +++ b/src/DictionaryEntry.Tests/BasicUsage/EntryTests.cs @@ -70,6 +70,25 @@ public void AndModify_WhenNotExists_DoesNothing() Assert.True(entry.IsVacant()); } + [Fact] + public void AndModify_WhenNotExists_DoesNotInvokeCallback() + { + // Arrange + var dict = new Dictionary(); + var invoked = false; + + // Act + dict.Entry("key").AndModify(value => + { + invoked = true; + return value + 1; + }); + + // Assert + Assert.False(invoked); + Assert.Empty(dict); + } + [Fact] public void OrInsert_WhenNotExists_InsertsValue() { @@ -85,6 +104,20 @@ public void OrInsert_WhenNotExists_InsertsValue() Assert.Equal(42, value); } + [Fact] + public void OrInsert_ReturnsReferenceToInsertedValue() + { + // Arrange + var dict = new Dictionary(); + + // Act + ref var valueRef = ref dict.Entry("key").OrInsert(42); + valueRef = 100; + + // Assert + Assert.Equal(100, dict["key"]); + } + [Fact] public void OrInsert_WhenExists_DoesNothing() { @@ -103,6 +136,23 @@ public void OrInsert_WhenExists_DoesNothing() Assert.Equal(42, value); } + [Fact] + public void OrInsert_WhenExists_ReturnsReferenceToExistingValue() + { + // Arrange + var dict = new Dictionary + { + ["key"] = 42 + }; + + // Act + ref var valueRef = ref dict.Entry("key").OrInsert(43); + valueRef = 100; + + // Assert + Assert.Equal(100, dict["key"]); + } + [Fact] public void OrInsertWith_InsertsValue() { @@ -134,18 +184,24 @@ public void OrInsertWithKey_WhenNotExists_InsertsValue() } [Fact] - public void OrInsertWithKey_WhenExists_DoesNothing() + public void OrInsertWithKey_WhenExists_DoesNotInvokeFactory() { // Arrange var dict = new Dictionary { ["key"] = 42 }; + var factoryCalled = false; // Act - var value = dict.Entry("key").OrInsertWithKey(key => key.Length); + ref var value = ref dict.Entry("key").OrInsertWithKey(key => + { + factoryCalled = true; + return key.Length; + }); // Assert + Assert.False(factoryCalled); Assert.True(dict.ContainsKey("key")); Assert.Equal(42, dict["key"]); Assert.Equal(42, value); @@ -166,6 +222,20 @@ public void OrDefault_WhenNotExists_InsertsDefaultValue() Assert.Equal(0, value); } + [Fact] + public void OrDefault_ReturnsReferenceAllowingMutation() + { + // Arrange + var dict = new Dictionary(); + + // Act + ref var valueRef = ref dict.Entry("key").OrDefault(); + valueRef = 7; + + // Assert + Assert.Equal(7, dict["key"]); + } + [Fact] public void OrDefault_WhenExists_DoesNothing() { From 34f245a3ba01e8abe20fd025d4d6d2f44795d872 Mon Sep 17 00:00:00 2001 From: Kyle Hickman Date: Thu, 13 Nov 2025 17:21:36 -0600 Subject: [PATCH 2/2] perf: adjust benchmarks --- DictionaryEntry.sln | 34 -------------- DictionaryEntry.slnx | 5 +++ .../BatchOperationsBenchmarks.cs | 17 +++---- .../PatternMatchingBenchmarks.cs | 32 ++++++-------- .../{ => BasicOps}/DefaultValueBenchmarks.cs | 13 +++--- .../BasicOps/DictionaryContainsBenchmarks.cs | 29 ++++++++++++ .../{ => BasicOps}/GetOrAddBenchmarks.cs | 20 ++++----- .../IncrementCounterBenchmarks.cs | 10 ++--- .../InitializeAbsentBenchmarks.cs | 10 ++--- .../RetrieveAndRemoveBenchmarks.cs | 17 +++---- .../BasicOps/TryGetEntryBenchmarks.cs | 44 +++++++++++++++++++ .../BenchmarkBase.cs | 12 +++++ .../ConditionalGetOrComputeBenchmarks.cs | 34 +++++++------- .../ConditionalModificationBenchmarks.cs | 13 +++--- .../InsertOrUpdateBenchmarks.cs | 16 +++---- .../UpdateExistingBenchmarks.cs | 16 +++---- .../{ => ConditionalOps}/UpsertBenchmarks.cs | 10 ++--- .../DictionaryEntry.Benchmarks.csproj | 7 +-- .../FactoryMethodBenchmarks.cs | 16 +++---- src/DictionaryEntry.Benchmarks/Program.cs | 14 +++++- .../Properties/launchSettings.json | 16 ------- 21 files changed, 201 insertions(+), 184 deletions(-) delete mode 100644 DictionaryEntry.sln create mode 100644 DictionaryEntry.slnx rename src/DictionaryEntry.Benchmarks/{ => AdvancedOps}/BatchOperationsBenchmarks.cs (85%) rename src/DictionaryEntry.Benchmarks/{ => AdvancedOps}/PatternMatchingBenchmarks.cs (77%) rename src/DictionaryEntry.Benchmarks/{ => BasicOps}/DefaultValueBenchmarks.cs (87%) create mode 100644 src/DictionaryEntry.Benchmarks/BasicOps/DictionaryContainsBenchmarks.cs rename src/DictionaryEntry.Benchmarks/{ => BasicOps}/GetOrAddBenchmarks.cs (73%) rename src/DictionaryEntry.Benchmarks/{ => BasicOps}/IncrementCounterBenchmarks.cs (90%) rename src/DictionaryEntry.Benchmarks/{ => BasicOps}/InitializeAbsentBenchmarks.cs (83%) rename src/DictionaryEntry.Benchmarks/{ => BasicOps}/RetrieveAndRemoveBenchmarks.cs (75%) create mode 100644 src/DictionaryEntry.Benchmarks/BasicOps/TryGetEntryBenchmarks.cs create mode 100644 src/DictionaryEntry.Benchmarks/BenchmarkBase.cs rename src/DictionaryEntry.Benchmarks/{ => ConditionalOps}/ConditionalGetOrComputeBenchmarks.cs (72%) rename src/DictionaryEntry.Benchmarks/{ => ConditionalOps}/ConditionalModificationBenchmarks.cs (85%) rename src/DictionaryEntry.Benchmarks/{ => ConditionalOps}/InsertOrUpdateBenchmarks.cs (75%) rename src/DictionaryEntry.Benchmarks/{ => ConditionalOps}/UpdateExistingBenchmarks.cs (72%) rename src/DictionaryEntry.Benchmarks/{ => ConditionalOps}/UpsertBenchmarks.cs (81%) rename src/DictionaryEntry.Benchmarks/{ => FactoryOps}/FactoryMethodBenchmarks.cs (87%) delete mode 100644 src/DictionaryEntry.Benchmarks/Properties/launchSettings.json diff --git a/DictionaryEntry.sln b/DictionaryEntry.sln deleted file mode 100644 index 74945c8..0000000 --- a/DictionaryEntry.sln +++ /dev/null @@ -1,34 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.0.31903.59 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DictionaryEntry", "src\DictionaryEntry\DictionaryEntry.csproj", "{6D68E402-5369-4881-B0B3-6C4C8C90C601}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DictionaryEntry.Benchmarks", "src\DictionaryEntry.Benchmarks\DictionaryEntry.Benchmarks.csproj", "{90CC74A9-2AE1-4DE6-8756-126CE0159DD0}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DictionaryEntry.Tests", "src\DictionaryEntry.Tests\DictionaryEntry.Tests.csproj", "{AF00BCB7-06E3-41BA-8312-00742EE9F5E6}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {6D68E402-5369-4881-B0B3-6C4C8C90C601}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {6D68E402-5369-4881-B0B3-6C4C8C90C601}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6D68E402-5369-4881-B0B3-6C4C8C90C601}.Release|Any CPU.ActiveCfg = Release|Any CPU - {6D68E402-5369-4881-B0B3-6C4C8C90C601}.Release|Any CPU.Build.0 = Release|Any CPU - {90CC74A9-2AE1-4DE6-8756-126CE0159DD0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {90CC74A9-2AE1-4DE6-8756-126CE0159DD0}.Debug|Any CPU.Build.0 = Debug|Any CPU - {90CC74A9-2AE1-4DE6-8756-126CE0159DD0}.Release|Any CPU.ActiveCfg = Release|Any CPU - {90CC74A9-2AE1-4DE6-8756-126CE0159DD0}.Release|Any CPU.Build.0 = Release|Any CPU - {AF00BCB7-06E3-41BA-8312-00742EE9F5E6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {AF00BCB7-06E3-41BA-8312-00742EE9F5E6}.Debug|Any CPU.Build.0 = Debug|Any CPU - {AF00BCB7-06E3-41BA-8312-00742EE9F5E6}.Release|Any CPU.ActiveCfg = Release|Any CPU - {AF00BCB7-06E3-41BA-8312-00742EE9F5E6}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection -EndGlobal diff --git a/DictionaryEntry.slnx b/DictionaryEntry.slnx new file mode 100644 index 0000000..ff7c289 --- /dev/null +++ b/DictionaryEntry.slnx @@ -0,0 +1,5 @@ + + + + + diff --git a/src/DictionaryEntry.Benchmarks/BatchOperationsBenchmarks.cs b/src/DictionaryEntry.Benchmarks/AdvancedOps/BatchOperationsBenchmarks.cs similarity index 85% rename from src/DictionaryEntry.Benchmarks/BatchOperationsBenchmarks.cs rename to src/DictionaryEntry.Benchmarks/AdvancedOps/BatchOperationsBenchmarks.cs index 34e79c8..e4c8130 100644 --- a/src/DictionaryEntry.Benchmarks/BatchOperationsBenchmarks.cs +++ b/src/DictionaryEntry.Benchmarks/AdvancedOps/BatchOperationsBenchmarks.cs @@ -1,11 +1,9 @@ -using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Attributes; -namespace DictionaryEntry.Benchmarks; +namespace DictionaryEntry.Benchmarks.AdvancedOps; -[MemoryDiagnoser] -[SimpleJob(invocationCount: 10_000_000)] -[BenchmarkCategory("BatchOperations")] -public class BatchOperationsBenchmarks +[BenchmarkCategory("AdvancedOps")] +public class BatchOperationsBenchmarks : BenchmarkBase { private Dictionary _dictionary = null!; private const string ExistingKey = "existing"; @@ -45,18 +43,17 @@ private void BatchOperationsEntry(string key) value = Math.Min(100, value); occupied.Insert(value); }, - vacant => vacant.Insert(5) - ); + vacant => vacant.Insert(5)); } [Benchmark(Baseline = true)] public void BatchOperations_Traditional_Exists() => BatchOperationsTraditional(ExistingKey); [Benchmark] - public void BatchOperations_Entry_Exists() => BatchOperationsEntry(ExistingKey); + public void BatchOperations_Traditional_NotExists() => BatchOperationsTraditional(NewKey); [Benchmark] - public void BatchOperations_Traditional_NotExists() => BatchOperationsTraditional(NewKey); + public void BatchOperations_Entry_Exists() => BatchOperationsEntry(ExistingKey); [Benchmark] public void BatchOperations_Entry_NotExists() => BatchOperationsEntry(NewKey); diff --git a/src/DictionaryEntry.Benchmarks/PatternMatchingBenchmarks.cs b/src/DictionaryEntry.Benchmarks/AdvancedOps/PatternMatchingBenchmarks.cs similarity index 77% rename from src/DictionaryEntry.Benchmarks/PatternMatchingBenchmarks.cs rename to src/DictionaryEntry.Benchmarks/AdvancedOps/PatternMatchingBenchmarks.cs index e01bf0a..6410878 100644 --- a/src/DictionaryEntry.Benchmarks/PatternMatchingBenchmarks.cs +++ b/src/DictionaryEntry.Benchmarks/AdvancedOps/PatternMatchingBenchmarks.cs @@ -1,11 +1,9 @@ -using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Attributes; -namespace DictionaryEntry.Benchmarks; +namespace DictionaryEntry.Benchmarks.AdvancedOps; -[MemoryDiagnoser] -[SimpleJob(invocationCount: 10_000_000)] -[BenchmarkCategory("PatternMatching")] -public class PatternMatchingBenchmarks +[BenchmarkCategory("AdvancedOps")] +public class PatternMatchingBenchmarks : BenchmarkBase { private Dictionary _dictionary = null!; private const string ExistingKey = "existing"; @@ -30,25 +28,22 @@ private string PatternMatchingTraditional(string key) _ => "Zero or negative" }; } + return "Not found"; } private string PatternMatchingEntry(string key) { return _dictionary.Entry(key).Match( - occupied => + occupied => occupied.Value() switch { - return occupied.Value() switch - { - > 100 => "Very large", - > 50 => "Large", - > 10 => "Medium", - > 0 => "Small", - _ => "Zero or negative" - }; + > 100 => "Very large", + > 50 => "Large", + > 10 => "Medium", + > 0 => "Small", + _ => "Zero or negative" }, - _ => "Not found" - ); + _ => "Not found"); } private void DifferentActionsTraditional(string key) @@ -67,8 +62,7 @@ private void DifferentActionsEntry(string key) { _dictionary.Entry(key).Match( occupied => occupied.Insert(occupied.Value() * 2), - vacant => vacant.Insert(1) - ); + vacant => vacant.Insert(1)); } [Benchmark(Baseline = true)] diff --git a/src/DictionaryEntry.Benchmarks/DefaultValueBenchmarks.cs b/src/DictionaryEntry.Benchmarks/BasicOps/DefaultValueBenchmarks.cs similarity index 87% rename from src/DictionaryEntry.Benchmarks/DefaultValueBenchmarks.cs rename to src/DictionaryEntry.Benchmarks/BasicOps/DefaultValueBenchmarks.cs index 4baee51..7f5a9db 100644 --- a/src/DictionaryEntry.Benchmarks/DefaultValueBenchmarks.cs +++ b/src/DictionaryEntry.Benchmarks/BasicOps/DefaultValueBenchmarks.cs @@ -1,12 +1,9 @@ -using BenchmarkDotNet.Attributes; -// ReSharper disable PreferConcreteValueOverDefault +using BenchmarkDotNet.Attributes; -namespace DictionaryEntry.Benchmarks; +namespace DictionaryEntry.Benchmarks.BasicOps; -[MemoryDiagnoser] -[SimpleJob(invocationCount: 10_000_000)] -[BenchmarkCategory("DefaultValue")] -public class DefaultValueBenchmarks +[BenchmarkCategory("BasicOps")] +public class DefaultValueBenchmarks : BenchmarkBase { private Dictionary _dictionary = null!; private Dictionary _stringDictionary = null!; @@ -28,6 +25,7 @@ private int DefaultValueTraditional(string key) value = default; _dictionary[key] = value; } + return value; } @@ -38,6 +36,7 @@ private int DefaultValueTraditional(string key) value = default; _stringDictionary[key] = value; } + return value; } diff --git a/src/DictionaryEntry.Benchmarks/BasicOps/DictionaryContainsBenchmarks.cs b/src/DictionaryEntry.Benchmarks/BasicOps/DictionaryContainsBenchmarks.cs new file mode 100644 index 0000000..28c519f --- /dev/null +++ b/src/DictionaryEntry.Benchmarks/BasicOps/DictionaryContainsBenchmarks.cs @@ -0,0 +1,29 @@ +using BenchmarkDotNet.Attributes; + +namespace DictionaryEntry.Benchmarks.BasicOps; + +[BenchmarkCategory("BasicOps")] +public class DictionaryContainsBenchmarks : BenchmarkBase +{ + private Dictionary _dictionary = null!; + private const int ExistingKey = 42; + private const int MissingKey = 21; + + [GlobalSetup] + public void Setup() + { + _dictionary = new Dictionary { { ExistingKey, 1 } }; + } + + [Benchmark(Baseline = true)] + public bool TryGetValue_Found() => _dictionary.TryGetValue(ExistingKey, out _); + + [Benchmark] + public bool Entry_IsOccupied_Found() => _dictionary.Entry(ExistingKey).IsOccupied(); + + [Benchmark] + public bool TryGetValue_NotFound() => _dictionary.TryGetValue(MissingKey, out _); + + [Benchmark] + public bool Entry_IsOccupied_NotFound() => _dictionary.Entry(MissingKey).IsOccupied(); +} diff --git a/src/DictionaryEntry.Benchmarks/GetOrAddBenchmarks.cs b/src/DictionaryEntry.Benchmarks/BasicOps/GetOrAddBenchmarks.cs similarity index 73% rename from src/DictionaryEntry.Benchmarks/GetOrAddBenchmarks.cs rename to src/DictionaryEntry.Benchmarks/BasicOps/GetOrAddBenchmarks.cs index 42ca30c..fb13c30 100644 --- a/src/DictionaryEntry.Benchmarks/GetOrAddBenchmarks.cs +++ b/src/DictionaryEntry.Benchmarks/BasicOps/GetOrAddBenchmarks.cs @@ -1,11 +1,9 @@ -using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Attributes; -namespace DictionaryEntry.Benchmarks; +namespace DictionaryEntry.Benchmarks.BasicOps; -[MemoryDiagnoser] -[SimpleJob(invocationCount: 10_000_000)] -[BenchmarkCategory("GetOrAdd")] -public class GetOrAddBenchmarks +[BenchmarkCategory("BasicOps")] +public class GetOrAddBenchmarks : BenchmarkBase { private Dictionary _dictionary = null!; private const int ExistingKey = 42; @@ -26,14 +24,14 @@ public void Cleanup() private int GetOrAddTraditional(int key) { - if (_dictionary.TryGetValue(key, out var val)) + if (_dictionary.TryGetValue(key, out var value)) { - return val; + return value; } - val = 1; - _dictionary[key] = val; - return val; + value = 1; + _dictionary[key] = value; + return value; } private int GetOrAddEntry(int key) diff --git a/src/DictionaryEntry.Benchmarks/IncrementCounterBenchmarks.cs b/src/DictionaryEntry.Benchmarks/BasicOps/IncrementCounterBenchmarks.cs similarity index 90% rename from src/DictionaryEntry.Benchmarks/IncrementCounterBenchmarks.cs rename to src/DictionaryEntry.Benchmarks/BasicOps/IncrementCounterBenchmarks.cs index d34815a..cd3e85a 100644 --- a/src/DictionaryEntry.Benchmarks/IncrementCounterBenchmarks.cs +++ b/src/DictionaryEntry.Benchmarks/BasicOps/IncrementCounterBenchmarks.cs @@ -1,11 +1,9 @@ -using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Attributes; -namespace DictionaryEntry.Benchmarks; +namespace DictionaryEntry.Benchmarks.BasicOps; -[MemoryDiagnoser] -[SimpleJob(invocationCount: 10_000_000)] -[BenchmarkCategory("IncrementCounter")] -public class IncrementCounterBenchmarks +[BenchmarkCategory("BasicOps")] +public class IncrementCounterBenchmarks : BenchmarkBase { private Dictionary _dictionary = null!; private const string ExistingKey = "existing"; diff --git a/src/DictionaryEntry.Benchmarks/InitializeAbsentBenchmarks.cs b/src/DictionaryEntry.Benchmarks/BasicOps/InitializeAbsentBenchmarks.cs similarity index 83% rename from src/DictionaryEntry.Benchmarks/InitializeAbsentBenchmarks.cs rename to src/DictionaryEntry.Benchmarks/BasicOps/InitializeAbsentBenchmarks.cs index ecd2973..593d7f3 100644 --- a/src/DictionaryEntry.Benchmarks/InitializeAbsentBenchmarks.cs +++ b/src/DictionaryEntry.Benchmarks/BasicOps/InitializeAbsentBenchmarks.cs @@ -1,11 +1,9 @@ -using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Attributes; -namespace DictionaryEntry.Benchmarks; +namespace DictionaryEntry.Benchmarks.BasicOps; -[MemoryDiagnoser] -[SimpleJob(invocationCount: 10_000_000)] -[BenchmarkCategory("InitializeAbsent")] -public class InitializeAbsentBenchmarks +[BenchmarkCategory("BasicOps")] +public class InitializeAbsentBenchmarks : BenchmarkBase { private Dictionary _dictionary = null!; private const string ExistingKey = "existing"; diff --git a/src/DictionaryEntry.Benchmarks/RetrieveAndRemoveBenchmarks.cs b/src/DictionaryEntry.Benchmarks/BasicOps/RetrieveAndRemoveBenchmarks.cs similarity index 75% rename from src/DictionaryEntry.Benchmarks/RetrieveAndRemoveBenchmarks.cs rename to src/DictionaryEntry.Benchmarks/BasicOps/RetrieveAndRemoveBenchmarks.cs index 1124708..f19adb1 100644 --- a/src/DictionaryEntry.Benchmarks/RetrieveAndRemoveBenchmarks.cs +++ b/src/DictionaryEntry.Benchmarks/BasicOps/RetrieveAndRemoveBenchmarks.cs @@ -1,11 +1,9 @@ -using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Attributes; -namespace DictionaryEntry.Benchmarks; +namespace DictionaryEntry.Benchmarks.BasicOps; -[MemoryDiagnoser] -[SimpleJob(invocationCount: 10_000_000)] -[BenchmarkCategory("GetAndRemove")] -public class GetAndRemoveBenchmarks +[BenchmarkCategory("BasicOps")] +public class RetrieveAndRemoveBenchmarks : BenchmarkBase { private Dictionary _dictionary = null!; private const string ExistingKey = "existing"; @@ -19,12 +17,7 @@ public void Setup() private int? GetAndRemoveTraditional(string key) { - if (_dictionary.Remove(key, out var value)) - { - return value; - } - - return null; + return _dictionary.Remove(key, out var value) ? value : null; } private int? GetAndRemoveEntry(string key) diff --git a/src/DictionaryEntry.Benchmarks/BasicOps/TryGetEntryBenchmarks.cs b/src/DictionaryEntry.Benchmarks/BasicOps/TryGetEntryBenchmarks.cs new file mode 100644 index 0000000..0891e68 --- /dev/null +++ b/src/DictionaryEntry.Benchmarks/BasicOps/TryGetEntryBenchmarks.cs @@ -0,0 +1,44 @@ +using BenchmarkDotNet.Attributes; + +namespace DictionaryEntry.Benchmarks.BasicOps; + +[BenchmarkCategory("BasicOps")] +public class TryGetEntryBenchmarks : BenchmarkBase +{ + private Dictionary _dictionary = null!; + private const string ExistingKey = "existing"; + private const string MissingKey = "missing"; + + [GlobalSetup] + public void Setup() + { + _dictionary = new Dictionary { { ExistingKey, 42 } }; + } + + private int? TryLookupTraditional(string key) + { + return _dictionary.TryGetValue(key, out var value) ? value : null; + } + + private int? TryLookupEntry(string key) + { + if (_dictionary.Entry(key).TryGetOccupied(out var occupied)) + { + return occupied.Value(); + } + + return null; + } + + [Benchmark(Baseline = true)] + public int? TryLookup_Traditional_Exists() => TryLookupTraditional(ExistingKey); + + [Benchmark] + public int? TryLookup_Traditional_NotExists() => TryLookupTraditional(MissingKey); + + [Benchmark] + public int? TryLookup_Entry_Exists() => TryLookupEntry(ExistingKey); + + [Benchmark] + public int? TryLookup_Entry_NotExists() => TryLookupEntry(MissingKey); +} diff --git a/src/DictionaryEntry.Benchmarks/BenchmarkBase.cs b/src/DictionaryEntry.Benchmarks/BenchmarkBase.cs new file mode 100644 index 0000000..8abdc02 --- /dev/null +++ b/src/DictionaryEntry.Benchmarks/BenchmarkBase.cs @@ -0,0 +1,12 @@ +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Engines; + +namespace DictionaryEntry.Benchmarks; + +[SimpleJob(RunStrategy.Throughput, iterationCount: 15, warmupCount: 10, invocationCount: 100_000_000)] +[MemoryDiagnoser(displayGenColumns: false)] +[HideColumns("Error", "StdDev", "Median", "Ratio", "Alloc Ratio", "RatioSD")] +[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByJob)] +[CategoriesColumn] +public abstract class BenchmarkBase; diff --git a/src/DictionaryEntry.Benchmarks/ConditionalGetOrComputeBenchmarks.cs b/src/DictionaryEntry.Benchmarks/ConditionalOps/ConditionalGetOrComputeBenchmarks.cs similarity index 72% rename from src/DictionaryEntry.Benchmarks/ConditionalGetOrComputeBenchmarks.cs rename to src/DictionaryEntry.Benchmarks/ConditionalOps/ConditionalGetOrComputeBenchmarks.cs index d9b0efb..b051340 100644 --- a/src/DictionaryEntry.Benchmarks/ConditionalGetOrComputeBenchmarks.cs +++ b/src/DictionaryEntry.Benchmarks/ConditionalOps/ConditionalGetOrComputeBenchmarks.cs @@ -1,11 +1,9 @@ -using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Attributes; -namespace DictionaryEntry.Benchmarks; +namespace DictionaryEntry.Benchmarks.ConditionalOps; -[MemoryDiagnoser] -[SimpleJob(invocationCount: 10_000_000)] -[BenchmarkCategory("ConditionalGetOrCompute")] -public class ConditionalGetOrCompute +[BenchmarkCategory("ConditionalOps")] +public class ConditionalGetOrComputeBenchmarks : BenchmarkBase { private Dictionary _dictionary = null!; private const string ExistingKey = "existing"; @@ -17,38 +15,38 @@ public void Setup() _dictionary = new Dictionary { { ExistingKey, 10 } }; } - private int ComputeValue() + private static int ComputeValue() { return DateTime.UtcNow.Millisecond % 100; } - private int ComputeValueFromString(string key) + private static int ComputeValueFromString(string key) { return key.Length; } private int GetOrComputeTraditional(string key) { - if (_dictionary.TryGetValue(key, out var val)) + if (_dictionary.TryGetValue(key, out var value)) { - return val; + return value; } - val = ComputeValue(); - _dictionary[key] = val; - return val; + value = ComputeValue(); + _dictionary[key] = value; + return value; } private int GetOrComputeTraditionalUsingKey(string key) { - if (_dictionary.TryGetValue(key, out var val)) + if (_dictionary.TryGetValue(key, out var value)) { - return val; + return value; } - val = ComputeValueFromString(key); - _dictionary[key] = val; - return val; + value = ComputeValueFromString(key); + _dictionary[key] = value; + return value; } private int GetOrComputeEntry(string key) diff --git a/src/DictionaryEntry.Benchmarks/ConditionalModificationBenchmarks.cs b/src/DictionaryEntry.Benchmarks/ConditionalOps/ConditionalModificationBenchmarks.cs similarity index 85% rename from src/DictionaryEntry.Benchmarks/ConditionalModificationBenchmarks.cs rename to src/DictionaryEntry.Benchmarks/ConditionalOps/ConditionalModificationBenchmarks.cs index 8a3787d..e6bd3a6 100644 --- a/src/DictionaryEntry.Benchmarks/ConditionalModificationBenchmarks.cs +++ b/src/DictionaryEntry.Benchmarks/ConditionalOps/ConditionalModificationBenchmarks.cs @@ -1,11 +1,9 @@ -using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Attributes; -namespace DictionaryEntry.Benchmarks; +namespace DictionaryEntry.Benchmarks.ConditionalOps; -[MemoryDiagnoser] -[SimpleJob(invocationCount: 10_000_000)] -[BenchmarkCategory("ConditionalModification")] -public class ConditionalModificationBenchmarks +[BenchmarkCategory("ConditionalOps")] +public class ConditionalModificationBenchmarks : BenchmarkBase { private Dictionary _dictionary = null!; private const string ExistingKey = "existing"; @@ -52,8 +50,7 @@ private void ConditionalModifyEntry(string key) occupied.Insert(value + 1); } }, - vacant => vacant.Insert(1) - ); + vacant => vacant.Insert(1)); } [Benchmark(Baseline = true)] diff --git a/src/DictionaryEntry.Benchmarks/InsertOrUpdateBenchmarks.cs b/src/DictionaryEntry.Benchmarks/ConditionalOps/InsertOrUpdateBenchmarks.cs similarity index 75% rename from src/DictionaryEntry.Benchmarks/InsertOrUpdateBenchmarks.cs rename to src/DictionaryEntry.Benchmarks/ConditionalOps/InsertOrUpdateBenchmarks.cs index 359ce88..7544397 100644 --- a/src/DictionaryEntry.Benchmarks/InsertOrUpdateBenchmarks.cs +++ b/src/DictionaryEntry.Benchmarks/ConditionalOps/InsertOrUpdateBenchmarks.cs @@ -1,11 +1,9 @@ -using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Attributes; -namespace DictionaryEntry.Benchmarks; +namespace DictionaryEntry.Benchmarks.ConditionalOps; -[MemoryDiagnoser] -[SimpleJob(invocationCount: 10_000_000)] -[BenchmarkCategory("InsertOrUpdate")] -public class UpdateOrInsertBenchmarks +[BenchmarkCategory("ConditionalOps")] +public class InsertOrUpdateBenchmarks : BenchmarkBase { private Dictionary _dictionary = null!; private const string ExistingKey = "existing"; @@ -19,10 +17,10 @@ public void Setup() private void InsertOrUpdateTraditional(string key) { - if (_dictionary.TryGetValue(key, out var val)) + if (_dictionary.TryGetValue(key, out var value)) { - val *= 2; - _dictionary[key] = val; + value *= 2; + _dictionary[key] = value; } else { diff --git a/src/DictionaryEntry.Benchmarks/UpdateExistingBenchmarks.cs b/src/DictionaryEntry.Benchmarks/ConditionalOps/UpdateExistingBenchmarks.cs similarity index 72% rename from src/DictionaryEntry.Benchmarks/UpdateExistingBenchmarks.cs rename to src/DictionaryEntry.Benchmarks/ConditionalOps/UpdateExistingBenchmarks.cs index 5dc7e84..6c82796 100644 --- a/src/DictionaryEntry.Benchmarks/UpdateExistingBenchmarks.cs +++ b/src/DictionaryEntry.Benchmarks/ConditionalOps/UpdateExistingBenchmarks.cs @@ -1,11 +1,9 @@ -using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Attributes; -namespace DictionaryEntry.Benchmarks; +namespace DictionaryEntry.Benchmarks.ConditionalOps; -[MemoryDiagnoser] -[SimpleJob(invocationCount: 10_000_000)] -[BenchmarkCategory("UpdateExisting")] -public class UpdateExistingBenchmarks +[BenchmarkCategory("ConditionalOps")] +public class UpdateExistingBenchmarks : BenchmarkBase { private Dictionary _dictionary = null!; private const string ExistingKey = "existing"; @@ -19,10 +17,10 @@ public void Setup() private void UpdateTraditional(string key) { - if (_dictionary.TryGetValue(key, out var val)) + if (_dictionary.TryGetValue(key, out var value)) { - val *= 2; - _dictionary[key] = val; + value *= 2; + _dictionary[key] = value; } } diff --git a/src/DictionaryEntry.Benchmarks/UpsertBenchmarks.cs b/src/DictionaryEntry.Benchmarks/ConditionalOps/UpsertBenchmarks.cs similarity index 81% rename from src/DictionaryEntry.Benchmarks/UpsertBenchmarks.cs rename to src/DictionaryEntry.Benchmarks/ConditionalOps/UpsertBenchmarks.cs index f213340..4f87ebf 100644 --- a/src/DictionaryEntry.Benchmarks/UpsertBenchmarks.cs +++ b/src/DictionaryEntry.Benchmarks/ConditionalOps/UpsertBenchmarks.cs @@ -1,11 +1,9 @@ -using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Attributes; -namespace DictionaryEntry.Benchmarks; +namespace DictionaryEntry.Benchmarks.ConditionalOps; -[MemoryDiagnoser] -[SimpleJob(invocationCount: 10_000_000)] -[BenchmarkCategory("Upsert")] -public class UpsertBenchmarks +[BenchmarkCategory("ConditionalOps")] +public class UpsertBenchmarks : BenchmarkBase { private Dictionary _dictionary = null!; private const string ExistingKey = "existing"; diff --git a/src/DictionaryEntry.Benchmarks/DictionaryEntry.Benchmarks.csproj b/src/DictionaryEntry.Benchmarks/DictionaryEntry.Benchmarks.csproj index 4872168..bdbfada 100644 --- a/src/DictionaryEntry.Benchmarks/DictionaryEntry.Benchmarks.csproj +++ b/src/DictionaryEntry.Benchmarks/DictionaryEntry.Benchmarks.csproj @@ -1,18 +1,19 @@ - + Exe net10.0 enable enable + true - + - + diff --git a/src/DictionaryEntry.Benchmarks/FactoryMethodBenchmarks.cs b/src/DictionaryEntry.Benchmarks/FactoryOps/FactoryMethodBenchmarks.cs similarity index 87% rename from src/DictionaryEntry.Benchmarks/FactoryMethodBenchmarks.cs rename to src/DictionaryEntry.Benchmarks/FactoryOps/FactoryMethodBenchmarks.cs index 2e0fafe..e24593e 100644 --- a/src/DictionaryEntry.Benchmarks/FactoryMethodBenchmarks.cs +++ b/src/DictionaryEntry.Benchmarks/FactoryOps/FactoryMethodBenchmarks.cs @@ -1,11 +1,9 @@ -using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Attributes; -namespace DictionaryEntry.Benchmarks; +namespace DictionaryEntry.Benchmarks.FactoryOps; -[MemoryDiagnoser] -[SimpleJob(invocationCount: 10_000_000)] -[BenchmarkCategory("FactoryMethod")] -public class FactoryMethodBenchmarks +[BenchmarkCategory("FactoryOps")] +public class FactoryMethodBenchmarks : BenchmarkBase { private Dictionary _dictionary = null!; private const string ExistingKey = "existing"; @@ -17,12 +15,12 @@ public void Setup() _dictionary = new Dictionary { { ExistingKey, 10 } }; } - private int ComputeExpensiveValue() + private static int ComputeExpensiveValue() { return DateTime.UtcNow.Second + DateTime.UtcNow.Minute; } - private int ComputeFromKey(string key) + private static int ComputeFromKey(string key) { return key.Length * 10; } @@ -34,6 +32,7 @@ private int FactoryMethodTraditional(string key) value = ComputeExpensiveValue(); _dictionary[key] = value; } + return value; } @@ -44,6 +43,7 @@ private int KeyBasedFactoryTraditional(string key) value = ComputeFromKey(key); _dictionary[key] = value; } + return value; } diff --git a/src/DictionaryEntry.Benchmarks/Program.cs b/src/DictionaryEntry.Benchmarks/Program.cs index 3c5f727..9faf0f4 100644 --- a/src/DictionaryEntry.Benchmarks/Program.cs +++ b/src/DictionaryEntry.Benchmarks/Program.cs @@ -1,4 +1,14 @@ -using System.Reflection; using BenchmarkDotNet.Running; -BenchmarkSwitcher.FromAssembly(Assembly.Load("DictionaryEntry.Benchmarks")).Run(args); +namespace DictionaryEntry.Benchmarks; + +public static class Program +{ + public static void Main(string[] args) + { + BenchmarkSwitcher + .FromAssembly(typeof(Program).Assembly) + .Run(args); + } +} + diff --git a/src/DictionaryEntry.Benchmarks/Properties/launchSettings.json b/src/DictionaryEntry.Benchmarks/Properties/launchSettings.json deleted file mode 100644 index 94df982..0000000 --- a/src/DictionaryEntry.Benchmarks/Properties/launchSettings.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "profiles": { - "Benchmarks": { - "commandName": "Project", - "commandLineArgs": "--join" - }, - "Benchmarks-Long": { - "commandName": "Project", - "commandLineArgs": "--filter * --job long --join" - }, - "Benchmarks-Short": { - "commandName": "Project", - "commandLineArgs": "--filter * --job short --join" - } - } -}