Recently, I made an interesting observation regarding Dictionary<string,T>
: the method TryGetValue()
is faster when building with new Dictionary<string,T>(StringComparer.Ordinal)
. This performance difference can be attributed to the fact that StringComparer.Ordinal
performs a raw byte comparison of string characters. I googled a bit and stumbled on this 2019 runtime note String Dictionary with StringComparer.Ordinal slower than default EqualityComparer.
I updated the benchmark to run against net7.0, net6.0, net481, net472
(see the benchmark code at the end) and got this result:
1 2 3 4 5 6 7 8 9 10 |
| Method | Job | Toolchain | Mean | Error | StdDev | Median | |------------------------ |----------- |--------------------- |---------:|---------:|---------:|---------:| | DefaultEqualityComparer | Job-JPQQTN | .NET 6.0 | 11.36 ns | 0.166 ns | 0.138 ns | 11.34 ns | | OrdinalEqualityComparer | Job-JPQQTN | .NET 6.0 | 12.05 ns | 0.334 ns | 0.948 ns | 11.70 ns | | DefaultEqualityComparer | Job-THKXVO | .NET 7.0 | 11.11 ns | 0.132 ns | 0.111 ns | 11.06 ns | | OrdinalEqualityComparer | Job-THKXVO | .NET 7.0 | 11.21 ns | 0.170 ns | 0.159 ns | 11.12 ns | | DefaultEqualityComparer | Job-WKIJAP | .NET Framework 4.7.2 | 18.89 ns | 0.243 ns | 0.215 ns | 18.86 ns | | OrdinalEqualityComparer | Job-WKIJAP | .NET Framework 4.7.2 | 16.21 ns | 0.114 ns | 0.089 ns | 16.25 ns | | DefaultEqualityComparer | Job-IDLIZV | .NET Framework 4.8.1 | 19.45 ns | 0.443 ns | 0.690 ns | 19.23 ns | | OrdinalEqualityComparer | Job-IDLIZV | .NET Framework 4.8.1 | 16.03 ns | 0.276 ns | 0.259 ns | 15.99 ns | |
Dictionary of string is faster with StringComparer.Ordinal
only on .NET Fx platform but slower on .NET Core ones that are so optimized that the default EqualityComparer
performs better.
So I built a method to use StringComparer.Ordinal
only within .NET Fx context. Let’s precise that most of NDepend code is compiled against netstandard2.0
since it runs within VisualStudio .NET Fx process. But it can also run on a Mac or Linux platform above .NET 5/6/7 and soon 8 (more on this here 5x Lessons Learned from Migrating a Large Legacy to .NET 5/6).
1 2 3 4 5 6 7 8 9 10 11 12 |
public static class DotNetRuntime { private static readonly DotNetRuntimeKind s_Kind; public static IDictionary<string, T> BuildDictionaryOfString<T>() { switch(s_Kind) { case DotNetRuntimeKind.NetFx: return new Dictionary<string, T>(StringComparer.Ordinal); default: return new Dictionary<string, T>(); } } } |
s_Kind
is inferred from this trick.
Now all calls to new Dictionary<string,T>()
must be refactored into calls to DotNetRuntime.BuildDictionaryOfString<T>()
.
Refactoring with Resharper Replace with Pattern
Thanks to recent enhancements in Visual Studio 2022 I find myself relying less on Resharper than before. However there is one Resharper awesome feature when it comes to refactoring Replace with Pattern also named Structural Search and Replace. One specific example of its usefulness is when searching for the pattern new Dictionary<string,$T$>()
with T
being a type expression 99 occurrences of new Dictionary<string,T>()
to refactor are matched.
To refactor these occurrences there is a Replace mode in the dialog Change Search Pattern that lets write this Replace pattern: DotNetRuntime.BuildDictionaryOfString<$T$>()
We can see how this feature outperforms a classic textual Find and Replace:
- It knows about C# and will find expression no matter the formatting (more or less space characters) and comments.
- Placeholder like
$T$
can be provided with a specific meaning. Here$T$
must be a type C# expression, but it could be an identifier, a statement, an argument or an expression. - Placeholder can be re-used in the Replace pattern to customize the refactoring.
Click Replace, then the next dialog let’s choose where exactly the refactoring should happen:
Finally Resharper does the job. However it was not smart enough to import the necessary using
statements so I had to do it manually:
Conclusion
This is a two in one post, with a micro-performance point for those that are still on .NET Framework and a cool Resharper Refactoring tool.
Interestingly enough benchmark shows that HashSet<T>
doesn’t run faster with StringComparer.Ordinal
.
The Benchmark Code
The C# code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
using System; using System.Collections.Generic; using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Configs; using BenchmarkDotNet.Environments; using BenchmarkDotNet.Jobs; using BenchmarkDotNet.Running; using BenchmarkDotNet.Toolchains.CsProj; namespace DictionaryOrdinalVsDefaultComparer { public class Program { [Config(typeof(MultipleRuntimes))] public class OrdinalVsDefaultComparer { private readonly Dictionary<string, string> _defaultDictionary = new Dictionary<string, string> { { "should be", "slower" } }; private readonly Dictionary<string, string> _ordinalDictionary = new Dictionary<string, string>(StringComparer.Ordinal) { { "should be", "faster" } }; [Benchmark] public string DefaultEqualityComparer() { _defaultDictionary.TryGetValue("should be", out var defaultComparerValue); return defaultComparerValue; } [Benchmark] public string OrdinalEqualityComparer() { _ordinalDictionary.TryGetValue("should be", out var ordinalComparerValue); return ordinalComparerValue; } } public static void Main(string[] args) { var summary = BenchmarkRunner.Run<OrdinalVsDefaultComparer>(); } public class MultipleRuntimes : ManualConfig { public MultipleRuntimes() { Add(Job.Default .With(CsProjCoreToolchain.NetCoreApp60) .With(Jit.RyuJit) .With(Platform.X64)); Add(Job.Default .With(CsProjCoreToolchain.NetCoreApp70) .With(Jit.RyuJit) .With(Platform.X64)); Add(Job.Default .With(CsProjClassicNetToolchain.Net481) .With(Jit.RyuJit) .With(Platform.X64)); Add(Job.Default .With(CsProjClassicNetToolchain.Net472) .With(Jit.RyuJit) .With(Platform.X64)); } } } } |
The .csproj content:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <OutputType>Exe</OutputType> <TargetFrameworks>net6.0;net7.0;net481;net472;</TargetFrameworks> <PlatformTarget>AnyCPU</PlatformTarget> <LangVersion>latest</LangVersion> </PropertyGroup> <PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Release|net7.0|AnyCPU'"> <DefineConstants /> </PropertyGroup> <ItemGroup> <PackageReference Include="BenchmarkDotNet" Version="0.13.5" /> <PackageReference Include="Microsoft.NETCore.Platforms" Version="7.0.2" /> </ItemGroup> </Project> |