Skip to content

Avoid per-call bool[] allocation in HeaderUtilities.AreEqualCollections#131230

Draft
mangod9 wants to merge 1 commit into
dotnet:mainfrom
mangod9:perf/headerutil-stackalloc-areequal
Draft

Avoid per-call bool[] allocation in HeaderUtilities.AreEqualCollections#131230
mangod9 wants to merge 1 commit into
dotnet:mainfrom
mangod9:perf/headerutil-stackalloc-areequal

Conversation

@mangod9

@mangod9 mangod9 commented Jul 22, 2026

Copy link
Copy Markdown
Member

Removes a per-call scratch-array allocation in HeaderUtilities.AreEqualCollections, which is invoked from the Equals overrides of parameterized header values (CacheControl, MediaType, ContentDisposition, Range, TransferCoding, NameValueWithParameters, ...).

Problem

AreEqualCollections allocated a new bool[x.Count] on every comparison. As the existing code comment notes, header parameter counts are almost always 1-2, so this is a small heap allocation on a comparison path.

Change

Use a stackalloc bool[32] buffer for the common small case, with an ArrayPool<bool> fallback (returned in a finally) for larger counts. Also replaces the Array.TrueForAll(alreadyFound, ...) closure in the Debug.Assert with an allocation-free Span check. Behavior is unchanged.

Validation

  • Builds clean across all System.Net.Http TFMs, 0 warnings.
  • System.Net.Http unit tests pass (2677/2677) - the header-value Equals tests exercise this path.

Note

This pull request was generated with the assistance of GitHub Copilot.

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<bool>
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>
Copilot AI review requested due to automatic review settings July 22, 2026 18:55
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 4 pipeline(s).
12 pipeline(s) were filtered out due to trigger conditions.
There may be pipelines that require an authorized user to comment /azp run to run.

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @karelz, @dotnet/ncl
See info in area-owners.md if you want to be subscribed.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR optimizes HeaderUtilities.AreEqualCollections (used by various header-value Equals implementations in System.Net.Http) by removing a per-call heap allocation in favor of stack allocation for the common small-count case, with an ArrayPool<bool> fallback for larger collections.

Changes:

  • Replaces new bool[x.Count] with a stackalloc-backed Span<bool> (thresholded) and ArrayPool<bool> for larger counts, returned via finally.
  • Replaces Array.TrueForAll(..., value => value) (closure) with an allocation-free Span<bool> check (Contains(false)) under Debug.Assert.


// 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];

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@AndyAyersMS this looks like a good candidate for escape analysis so all the unsafe code with stackalloc + arraypool becomes unnecessary

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#127980 will conditionally stack allocate this array. Its threshold is dynamic depending on how much free space exists on the stack. However the fallback is a heap allocation, not an array pool rental.

If we wanted to model this exact pattern we'd have to add quite a bit more logic to #127980 and some hellper-call accessible versions of rent/return.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we can rewrite it to just bool[] alreadyFound = x.Count <= 32 ? new bool[32] : new bool[x.Count]; and it will just work we can assume the number (headers) is rather low.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants