From 1ac3a1ab75df5f5c27e6f3d144b4047d573c5141 Mon Sep 17 00:00:00 2001 From: mangod9 Date: Wed, 22 Jul 2026 11:48:39 -0700 Subject: [PATCH] Avoid per-call bool[] allocation in HeaderUtilities.AreEqualCollections AreEqualCollections allocated a new bool[x.Count] scratch array on every header value equality comparison (CacheControl/MediaType/ContentDisposition/Range/ TransferCoding/NameValueWithParameters). Header parameter counts are almost always 1-2, so use a stackalloc buffer for small counts with an ArrayPool fallback for larger ones. Also replaces the Array.TrueForAll closure in the debug assert with an allocation-free Span check. Behavior-preserving; System.Net.Http unit tests pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Net/Http/Headers/HeaderUtilities.cs | 62 ++++++++++++------- 1 file changed, 38 insertions(+), 24 deletions(-) diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/Headers/HeaderUtilities.cs b/src/libraries/System.Net.Http/src/System/Net/Http/Headers/HeaderUtilities.cs index 54bb829a82aa9b..5c26144a77e23a 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/Headers/HeaderUtilities.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/Headers/HeaderUtilities.cs @@ -201,41 +201,55 @@ internal static bool AreEqualCollections(ObjectCollection? x, ObjectCollec // We have two unordered lists. So comparison is an O(n*m) operation which is expensive. Usually // headers have 1-2 parameters (if any), so this comparison shouldn't be too expensive. - bool[] alreadyFound = new bool[x.Count]; - int i = 0; - foreach (var xItem in x) + const int StackallocThreshold = 32; + int count = x.Count; + bool[]? rentedArray = count > StackallocThreshold ? ArrayPool.Shared.Rent(count) : null; + Span alreadyFound = rentedArray is null ? stackalloc bool[StackallocThreshold] : rentedArray; + alreadyFound = alreadyFound.Slice(0, count); + alreadyFound.Clear(); + try { - Debug.Assert(xItem != null); - - i = 0; - bool found = false; - foreach (var yItem in y) + foreach (var xItem in x) { - if (!alreadyFound[i]) + Debug.Assert(xItem != null); + + int i = 0; + bool found = false; + foreach (var yItem in y) { - if (((comparer == null) && xItem.Equals(yItem)) || - ((comparer != null) && comparer.Equals(xItem, yItem))) + if (!alreadyFound[i]) { - alreadyFound[i] = true; - found = true; - break; + if (((comparer == null) && xItem.Equals(yItem)) || + ((comparer != null) && comparer.Equals(xItem, yItem))) + { + alreadyFound[i] = true; + found = true; + break; + } } + i++; + } + + if (!found) + { + return false; } - i++; } - if (!found) + // Since we never re-use a "found" value in 'y', we expect 'alreadyFound' to have all fields set to 'true'. + // Otherwise the two collections can't be equal and we should not get here. + Debug.Assert(!alreadyFound.Contains(false), + "Expected all values in 'alreadyFound' to be true since collections are considered equal."); + + return true; + } + finally + { + if (rentedArray is not null) { - return false; + ArrayPool.Shared.Return(rentedArray); } } - - // Since we never re-use a "found" value in 'y', we expect 'alreadyFound' to have all fields set to 'true'. - // Otherwise the two collections can't be equal and we should not get here. - Debug.Assert(Array.TrueForAll(alreadyFound, value => value), - "Expected all values in 'alreadyFound' to be true since collections are considered equal."); - - return true; } internal static int GetNextNonEmptyOrWhitespaceIndex(string input, int startIndex, bool skipEmptyValues,