Skip to content

Optimize response negotiator selection performance#385

Merged
jchannon merged 1 commit into
CarterCommunity:mainfrom
seifmaazouz:perf/response-negotiator
Jul 4, 2026
Merged

Optimize response negotiator selection performance#385
jchannon merged 1 commit into
CarterCommunity:mainfrom
seifmaazouz:perf/response-negotiator

Conversation

@seifmaazouz

@seifmaazouz seifmaazouz commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

What changed

While looking through the response negotiation path, I noticed a few unnecessary allocations and LINQ operations in code that runs on every negotiated response.

This PR improves it by:

  • Replacing List<IResponseNegotiator> with an array snapshot
  • Removing LINQ from the negotiation path (OrderByDescending, First, FirstOrDefault)
  • Replacing enumeration-based lookups with simple loops
  • Adding a small stable insertion sort for Accept header quality ordering
  • Extracting the default JSON negotiator lookup into a shared helper

No behavior changes are intended.


Why

The negotiation path runs on every negotiated response, so even small allocations and LINQ overhead add up.

This keeps the existing behavior while reducing allocations and lowering the cost of the hot path.

The insertion sort is intentional here since Accept headers are typically very small, making it a simple, allocation-free choice while preserving the ordering of equal quality values.


Benchmark

I benchmarked the original implementation against this version using BenchmarkDotNet.

Case Before After
1 negotiator 131 ns 25 ns
3 negotiators 129 ns 27 ns
8 negotiators 134 ns 31 ns

Memory allocations were also reduced significantly:

  • 1 negotiator: 456 B → 32 B
  • 3 negotiators: 472 B → 48 B
  • 8 negotiators: 512 B → 88 B

Benchmark output

BenchmarkDotNet results

The benchmark covers a few representative scenarios:

  • NegotiatorCount varies the number of registered response negotiators (1, 3 and 8).
  • MultipleAcceptHeaders compares requests with one accepted media type against requests with multiple accepted media types.
  • Mean is the average execution time.
  • Allocated is the memory allocated per operation.

Notes

  • No breaking changes
  • Existing tests pass
  • Focused on reducing overhead in the response negotiation path

@jchannon jchannon added this to the vNext milestone Jul 4, 2026
@jchannon jchannon merged commit 42055e5 into CarterCommunity:main Jul 4, 2026
1 check passed
@jchannon

jchannon commented Jul 4, 2026

Copy link
Copy Markdown
Member

Thanks. Great work 👍

@seifmaazouz seifmaazouz deleted the perf/response-negotiator branch July 4, 2026 10:23
@seifmaazouz seifmaazouz restored the perf/response-negotiator branch July 4, 2026 10:23
@seifmaazouz seifmaazouz deleted the perf/response-negotiator branch July 4, 2026 10:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants