Repository navigation
perf(engine): Write the HTTP/1.1 response head in a single pass - #974
Conversation
x64 benchmarksThe gain holds on x64 as well. The PR was faster than the baseline on all four benchmarks in every round. Setup: Intel i7-13800H (Windows 11, x64), .NET 11 rc3, net11.0, in-process BenchmarkDotNet 0.15.8, middleware-only ( Mean of 4 rounds:
Change per round,
An earlier pass of 3 rounds (baseline vs. Allocations are unchanged (0 B for the first three, 24 B for the webservice).
* These are my own versions of the ad-hoc benchmarks described above, not the exact code used on the Pi. They are not part of this PR. Tests: 🤖 Generated with Claude Code |
ResponseSerializer issued 15-25 small writes per response, each with its own GetSpan/Advance pair on the PipeWriter. The head size is now computed up front and everything is copied into one span with a single Advance. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016Z4d2PxGuaT75fzUxMqgyN
The custom headers are now scanned once to compute their size and to find Server or Date, instead of two extra ContainsKey passes. The span requested for the head uses the exact Content-Length digit count. Also documents ShouldSendBody. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
dc84091 to
1999799
Compare
Summary
ResponseSerializer.WriteHeadissued 15–25 smallwriter.Writecalls per response, each with its ownGetSpan/Advancepair on thePipeWriter. It now adds up the head size first, takes one span and copies the status line,Server/Date, connection, content and custom headers into it with a singleAdvance.Type,Length,Encoding) are read once.Content-Lengthis formatted in place (up to 20 digits reserved).BufferWriterExtensions.Write(ulong)had no callers left, so it is removed (internal).Affects HTTP/1.1 responses on the Internal and Ioxide engines. Kestrel writes its own headers and is unchanged. Output bytes are identical, so users will not notice any behavior change.
Benchmarks
Raspberry Pi 4 (Cortex-A72, arm64, no LSE atomics), .NET 11 rc2, in-process BenchmarkDotNet, middleware-only (
BenchmarkContext). Means over 5–6 runs ofmainand 4 runs of this branch, interleaved. Run-to-run noise on the Pi is about ±5%.SimpleBenchmark(Hello World)BaselineBenchmarkAllocations are unchanged (0 B for the first three, 1.01 KB for the webservice).
* These were ad-hoc benchmarks for this measurement and are not part of this PR. I can add them to
Benchmarks/if they are useful.Tests
HeaderTests.TestLargeHeadersAreWritten(multi-engine): a 20 KB header, larger than one pipe segment, plus a small header and a body all arrive intact.dotnet build GenHTTP.slnx -c Releasepasses.TestHttp2And3, which also fails onmainhere (no msquic on the Pi). Ioxide could not run here becauseio_uringhits the Pi's 8 MBRLIMIT_MEMLOCK(the test host crashes inRing.Create/pbuf_ring), so CI will verify Ioxide.Also tried and dropped
Other ideas from the arm64 list were measured on the same Pi but are not included:
Ascii.EqualsIgnoreCase: 26–39% faster in isolation, but about 6% slower end-to-end on the realistic request in every run.🤖 Generated with Claude Code
https://claude.ai/code/session_016Z4d2PxGuaT75fzUxMqgyN