From 9d47c0004771290f6f94953386cfac4677be39fe Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Wed, 3 May 2023 12:12:54 -0500 Subject: [PATCH] [core] use StringComparer for Dictionary/HashSet (#14900) Context: https://github.com/dotnet/maui/issues/12130 Context: https://github.com/angelru/CvSlowJittering When profiling the above customer app while scrolling, 4% of the time is spent doing dictionary lookups: (4.0%) System.Private.CoreLib!System.Collections.Generic.Dictionary.FindValue(TKey_REF) Observing the call stack, some of these are coming from culture-aware string lookups in MAUI: * `microsoft.maui!Microsoft.Maui.PropertyMapper.GetProperty(string)` * `microsoft.maui!Microsoft.Maui.WeakEventManager.AddEventHandler(System.EventHandler`1,string)` * `microsoft.maui!Microsoft.Maui.CommandMapper.GetCommand(string)` Which show up as a mixture of `string` comparers: (0.98%) System.Private.CoreLib!System.Collections.Generic.NonRandomizedStringEqualityComparer.OrdinalComparer.GetHashCode(string) (0.71%) System.Private.CoreLib!System.String.GetNonRandomizedHashCode() (0.31%) System.Private.CoreLib!System.Collections.Generic.NonRandomizedStringEqualityComparer.OrdinalComparer.Equals(string,stri (0.01%) System.Private.CoreLib!System.Collections.Generic.NonRandomizedStringEqualityComparer.GetStringComparer(object) In cases of `Dictionary` or `HashSet`, we can use `StringComparer.Ordinal` for faster dictionary lookups. Unfortunately, there is no code analyzer for this: https://github.com/dotnet/runtime/issues/52399 So, I manually went through the codebase and found all the places. I now only see the *fast* string comparers in this sample: (1.3%) System.Private.CoreLib!System.Collections.Generic.NonRandomizedStringEqualityComparer.OrdinalComparer.GetHashCode(string) (0.35%) System.Private.CoreLib!System.Collections.Generic.NonRandomizedStringEqualityComparer.OrdinalComparer.Equals(string,stri Which is about ~0.36% better than before. This should slightly improve the performance of handlers & all controls on all platforms. I also fixed `Microsoft.Maui.Graphics.Text.TextColors` to use `StringComparer.OrdinalIgnoreCase` -- and removed a `ToUpperInvariant()` call. --- src/TextToSpeech/TextToSpeech.android.cs | 2 +- src/Types/Shared/WebUtils.shared.cs | 2 +- src/VersionTracking/VersionTracking.shared.cs | 4 ++-- src/WebAuthenticator/WebAuthenticatorResult.shared.cs | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/TextToSpeech/TextToSpeech.android.cs b/src/TextToSpeech/TextToSpeech.android.cs index db4dfa2..8f2ce2d 100644 --- a/src/TextToSpeech/TextToSpeech.android.cs +++ b/src/TextToSpeech/TextToSpeech.android.cs @@ -157,7 +157,7 @@ public async Task SpeakAsync(string text, int max, SpeechOptions options, Cancel for (var i = 0; i < parts.Count && !cancelToken.IsCancellationRequested; i++) { // We require the utterance id to be set if we want the completed listener to fire - var map = new Dictionary + var map = new Dictionary(StringComparer.Ordinal) { { AndroidTextToSpeech.Engine.KeyParamUtteranceId, $"{guid}.{i}" } }; diff --git a/src/Types/Shared/WebUtils.shared.cs b/src/Types/Shared/WebUtils.shared.cs index 169038b..e454f06 100644 --- a/src/Types/Shared/WebUtils.shared.cs +++ b/src/Types/Shared/WebUtils.shared.cs @@ -10,7 +10,7 @@ static class WebUtils { internal static IDictionary ParseQueryString(string url) { - var d = new Dictionary(); + var d = new Dictionary(StringComparer.Ordinal); if (string.IsNullOrWhiteSpace(url) || (url.IndexOf("?", StringComparison.Ordinal) == -1 && url.IndexOf("#", StringComparison.Ordinal) == -1)) return d; diff --git a/src/VersionTracking/VersionTracking.shared.cs b/src/VersionTracking/VersionTracking.shared.cs index 5de4ea4..3dfa506 100644 --- a/src/VersionTracking/VersionTracking.shared.cs +++ b/src/VersionTracking/VersionTracking.shared.cs @@ -238,7 +238,7 @@ internal void InitVersionTracking() IsFirstLaunchEver = !preferences.ContainsKey(versionsKey, sharedName) || !preferences.ContainsKey(buildsKey, sharedName); if (IsFirstLaunchEver) { - versionTrail = new Dictionary> + versionTrail = new(StringComparer.Ordinal) { { versionsKey, new List() }, { buildsKey, new List() } @@ -246,7 +246,7 @@ internal void InitVersionTracking() } else { - versionTrail = new Dictionary> + versionTrail = new(StringComparer.Ordinal) { { versionsKey, ReadHistory(versionsKey).ToList() }, { buildsKey, ReadHistory(buildsKey).ToList() } diff --git a/src/WebAuthenticator/WebAuthenticatorResult.shared.cs b/src/WebAuthenticator/WebAuthenticatorResult.shared.cs index 53a7ff7..c54b8ce 100644 --- a/src/WebAuthenticator/WebAuthenticatorResult.shared.cs +++ b/src/WebAuthenticator/WebAuthenticatorResult.shared.cs @@ -73,7 +73,7 @@ public WebAuthenticatorResult(IDictionary properties) /// /// The dictionary of key/value pairs parsed form the callback URI's query string. /// - public Dictionary Properties { get; set; } = new Dictionary(); + public Dictionary Properties { get; set; } = new(StringComparer.Ordinal); /// Puts a key/value pair into the dictionary. public void Put(string key, string value)