From 5c374f19666c4bbe0bca464264d0a0f4ae306cdf Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Mon, 2 Mar 2026 22:13:33 +0000 Subject: [PATCH] Add SelectionSet small set optimization --- .../Execution/Processing/SelectionSet.cs | 136 +++++++++++++++++- .../Execution/Nodes/SelectionSet.cs | 136 +++++++++++++++++- 2 files changed, 270 insertions(+), 2 deletions(-) diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/SelectionSet.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/SelectionSet.cs index 5f1f0db2702..a620ae903b4 100644 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/SelectionSet.cs +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/SelectionSet.cs @@ -1,5 +1,6 @@ using System.Collections.Frozen; using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; using System.Text; using HotChocolate.Types; @@ -107,8 +108,141 @@ public bool TryGetSelection(string responseName, [NotNullWhen(true)] out Selecti /// /// Returns true if the selection was successfully resolved. /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool TryGetSelection(ReadOnlySpan utf8ResponseName, [NotNullWhen(true)] out Selection? selection) - => _utf8ResponseNameLookup.TryGetSelection(utf8ResponseName, out selection); + { + // Many execution selection sets are tiny (1-4 fields), so direct + // span comparisons avoid hash computation/probing in the hot path. + var selections = _selections; + + switch (selections.Length) + { + case 1: + var candidate = selections[0]; + + if (utf8ResponseName.SequenceEqual(candidate.Utf8ResponseName)) + { + selection = candidate; + return true; + } + + selection = default; + return false; + + case 2: + var candidate0 = selections[0]; + + if (utf8ResponseName.SequenceEqual(candidate0.Utf8ResponseName)) + { + selection = candidate0; + return true; + } + + var candidate1 = selections[1]; + + if (utf8ResponseName.SequenceEqual(candidate1.Utf8ResponseName)) + { + selection = candidate1; + return true; + } + + selection = default; + return false; + + case 3: + var firstCandidate = selections[0]; + + if (utf8ResponseName.SequenceEqual(firstCandidate.Utf8ResponseName)) + { + selection = firstCandidate; + return true; + } + + var secondCandidate = selections[1]; + + if (utf8ResponseName.SequenceEqual(secondCandidate.Utf8ResponseName)) + { + selection = secondCandidate; + return true; + } + + var thirdCandidate = selections[2]; + + if (utf8ResponseName.SequenceEqual(thirdCandidate.Utf8ResponseName)) + { + selection = thirdCandidate; + return true; + } + + selection = default; + return false; + + case 4: + var firstCandidate4 = selections[0]; + + if (utf8ResponseName.SequenceEqual(firstCandidate4.Utf8ResponseName)) + { + selection = firstCandidate4; + return true; + } + + var secondCandidate4 = selections[1]; + + if (utf8ResponseName.SequenceEqual(secondCandidate4.Utf8ResponseName)) + { + selection = secondCandidate4; + return true; + } + + var thirdCandidate4 = selections[2]; + + if (utf8ResponseName.SequenceEqual(thirdCandidate4.Utf8ResponseName)) + { + selection = thirdCandidate4; + return true; + } + + var fourthCandidate4 = selections[3]; + + if (utf8ResponseName.SequenceEqual(fourthCandidate4.Utf8ResponseName)) + { + selection = fourthCandidate4; + return true; + } + + selection = default; + return false; + + } + + if (selections.Length <= 7) + { + return TryGetSelectionLinear(utf8ResponseName, selections, out selection); + } + + return _utf8ResponseNameLookup.TryGetSelection(utf8ResponseName, out selection); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool TryGetSelectionLinear( + ReadOnlySpan utf8ResponseName, + Selection[] selections, + [NotNullWhen(true)] out Selection? selection) + { + for (var i = 0; i < selections.Length; i++) + { + var candidate = selections[i]; + + if (utf8ResponseName.SequenceEqual(candidate.Utf8ResponseName)) + { + selection = candidate; + return true; + } + } + + selection = default; + return false; + } internal void Complete(Operation declaringOperation) { diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/SelectionSet.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/SelectionSet.cs index 4b4dca60b11..8f8e08a96e2 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/SelectionSet.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/SelectionSet.cs @@ -1,5 +1,6 @@ using System.Collections.Frozen; using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; using HotChocolate.Execution; using HotChocolate.Types; @@ -92,8 +93,141 @@ public bool TryGetSelection(string responseName, [NotNullWhen(true)] out Selecti /// /// Returns true if the selection was successfully resolved. /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool TryGetSelection(ReadOnlySpan utf8ResponseName, [NotNullWhen(true)] out Selection? selection) - => _utf8ResponseNameLookup.TryGetSelection(utf8ResponseName, out selection); + { + // Many execution selection sets are tiny (1-4 fields), so direct + // span comparisons avoid hash computation/probing in the hot path. + var selections = _selections; + + switch (selections.Length) + { + case 1: + var candidate = selections[0]; + + if (utf8ResponseName.SequenceEqual(candidate.Utf8ResponseName)) + { + selection = candidate; + return true; + } + + selection = default; + return false; + + case 2: + var candidate0 = selections[0]; + + if (utf8ResponseName.SequenceEqual(candidate0.Utf8ResponseName)) + { + selection = candidate0; + return true; + } + + var candidate1 = selections[1]; + + if (utf8ResponseName.SequenceEqual(candidate1.Utf8ResponseName)) + { + selection = candidate1; + return true; + } + + selection = default; + return false; + + case 3: + var firstCandidate = selections[0]; + + if (utf8ResponseName.SequenceEqual(firstCandidate.Utf8ResponseName)) + { + selection = firstCandidate; + return true; + } + + var secondCandidate = selections[1]; + + if (utf8ResponseName.SequenceEqual(secondCandidate.Utf8ResponseName)) + { + selection = secondCandidate; + return true; + } + + var thirdCandidate = selections[2]; + + if (utf8ResponseName.SequenceEqual(thirdCandidate.Utf8ResponseName)) + { + selection = thirdCandidate; + return true; + } + + selection = default; + return false; + + case 4: + var firstCandidate4 = selections[0]; + + if (utf8ResponseName.SequenceEqual(firstCandidate4.Utf8ResponseName)) + { + selection = firstCandidate4; + return true; + } + + var secondCandidate4 = selections[1]; + + if (utf8ResponseName.SequenceEqual(secondCandidate4.Utf8ResponseName)) + { + selection = secondCandidate4; + return true; + } + + var thirdCandidate4 = selections[2]; + + if (utf8ResponseName.SequenceEqual(thirdCandidate4.Utf8ResponseName)) + { + selection = thirdCandidate4; + return true; + } + + var fourthCandidate4 = selections[3]; + + if (utf8ResponseName.SequenceEqual(fourthCandidate4.Utf8ResponseName)) + { + selection = fourthCandidate4; + return true; + } + + selection = default; + return false; + + } + + if (selections.Length <= 7) + { + return TryGetSelectionLinear(utf8ResponseName, selections, out selection); + } + + return _utf8ResponseNameLookup.TryGetSelection(utf8ResponseName, out selection); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool TryGetSelectionLinear( + ReadOnlySpan utf8ResponseName, + Selection[] selections, + [NotNullWhen(true)] out Selection? selection) + { + for (var i = 0; i < selections.Length; i++) + { + var candidate = selections[i]; + + if (utf8ResponseName.SequenceEqual(candidate.Utf8ResponseName)) + { + selection = candidate; + return true; + } + } + + selection = default; + return false; + } internal void Seal(Operation operation) {