Repository navigation
pipes: AdvanceTo on a reader holding one recv computes its offsets from the positions themselves, without three GetOffset calls - #400
Merged
Conversation
…om the positions themselves, without three GetOffset calls TcpConnectionPipeReader.AdvanceTo turned both positions into byte offsets with three ReadOnlySequence.GetOffset calls - the sequence start, consumed and examined - rebasing on the start because GetOffset counts from the start segment. Each call bounds-checks, and across segments walks the chain. Most reads hand out one recv slice, and a position into it is that slice and an index into its memory, so the offset is the index less what was already consumed. AdvanceTo now does that when one slice is held, both positions are on it and both indexes are inside what it still holds. Anything else - more slices, a position on another segment, an index out of range - takes the GetOffset path as before, which also keeps refusing a position the sequence does not hold. A micro-benchmark of the offset step (one recv slice, partly consumed, everything consumed) puts the three GetOffset calls at 6.9-7.3 ns, which is what this skips. End to end that is below what this box resolves: wrk on Tcp/Pipe, 12 paired rounds against main, user CPU per request -1.2%, against -1.9% on Tcp/Raw, which does not use the pipe reader, and +0.6% for the main-vs-main control. Test (E2E, PipeReaderContractTests): AdvanceTo refuses a position outside what it handed out, past a recv's end and inside its consumed part, and the body stays readable after both refusals; it asserts the request arrived in one recv, so the positions take the new path, and reports an accepted position at once, since the counters are wrong from then on and the next read would park. Red-checked: with no range checks in the fast path it fails with "past the recv's end: accepted", and with an upper bound only (a signed compare) with "inside its consumed part: accepted"; the fast path as committed passes.
… unconsumed bytes, and applies only to the last result's sequence From review of the first commit. Its bound was _heldBytes, which equals the one recv's unconsumed length only while the counters are right; once a caller had broken them (two AdvanceTo for one read, twice) any index on the recv passed. The bound is now the recv's own unconsumed length, the one GetOffset's bounds check uses, so it holds whatever the counters say. And the path applies only when the last result's sequence starts at that recv, so a stale or reset sequence - the overflow error paths and ReleaseHeld leave one - takes the GetOffset path as before. The comment no longer claims GetOffset refuses a position on another segment, which for one segment it does not; the casts that turn a negative offset into an out-of-range one are explicit about wrapping. The test now also refuses an examined position past the recv's end, and a new one refuses a position past the first recv while the reader holds two, which a bound over everything held would accept.
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
TcpConnectionPipeReader.AdvanceToturned both positions into byte offsets with threeReadOnlySequence.GetOffsetcalls: one for the sequence start, then one each forconsumedandexamined. It rebases on the start becauseGetOffsetcounts from the start segment, not from where the sequence begins. Each call bounds-checks and, across segments, walks the chain.Most reads hand out one recv slice, and then none of that is needed. A position into the slice is the slice and an index into its memory, so the offset is the index less what was already consumed.
What changes:
GetOffset's bounds check uses. Offsets are then computed from the indexes directly.GetOffsetpath unchanged, which also keeps refusing an index the sequence doesn't hold.After a read, the two paths compute the same offsets and refuse the same positions. They differ only on an
AdvanceTowith no read since the last result, which thePipeReadercontract forbids (Pipethrows for it):From review
An adversarial review of the first commit tested the equivalence with a harness. The harness compiles main's
AdvanceToand this one verbatim, runs both from the same states, and compares exceptions, offsets and the state they leave. The equivalence held after every read. The review found five smaller things, all fixed in the second commit:_heldBytes. That equals the slice's unconsumed length only while the counters are right. Once two contract violations had made them negative, any index on the slice passed, and a read could then return empty results forever. The bound is now the slice's length less what is consumed, so it holds whatever the counters say._lastSequence. The overflow error paths andReleaseHeldleave one. The fast path now applies only when the last result's sequence starts on the head slice.GetOffsetrefuses a position on another segment. For a one-segment sequence it doesn't, on main either.unchecked.consumedbound. It now checksexaminedtoo, and a new test covers a read holding two recvs (below).The same harness on the second commit, with two seeds:
In that last row, this PR never leaves the counters wrong where main's stay right. Main's go wrong where these stay right in about 10K calls per seed.
Measured
Per
AdvanceTo, with a micro-benchmark of the offset step pinned to one P-core: one recv slice, partly consumed, the caller consuming everything held, 10 alternating rounds of 50M each.GetOffsetcalls, which take 7.09 ns.End to end, it is below what this box resolves. These figures are for the first commit; the second adds a reference compare and a length load to the fast path.
wrkonTcp/Pipe, measured as in the pipe-adapter PR (#399):wrk -t4 -c64on other cores.Tcp/PipeTcp/Raw(doesn't use the pipe reader)Tcp/PipeSeven nanoseconds is about 4% of the 164 ns of user-mode CPU a Pipe request costs here, and under 0.4% of its total CPU. The Raw row, which this change cannot affect, moves as much as the Pipe one.
Tests
In
PipeReaderContractTests(E2E):pipereader: AdvanceTo refuses a position outside what it handed out, past a recv's end and inside its consumed partAdvanceToa position one past the recv's end, asconsumedand asexamined, then consumes the header and hands it a position inside the consumed header. Every one must be refused, and the body must still read whole afterwards.pipereader: AdvanceTo refuses a position past the first recv while the reader holds two, new in the second commitRed-checked, one break per variant:
got [past the recv's end: accepted]got [inside its consumed part: accepted]examinedrange checkgot [examined past the recv's end: accepted]got [past the first recv: accepted]The rest of
AdvanceTois covered as before:PipeReaderContractTests: examined-to-the-end, a partial consume of one recv, and buffers returning as consumption passes them.RecvBufferReclaimTests.All suites pass: Unit 105, E2E 328, Http 89, Tls 194, Chaos 80, File 5, Pg 7 (+11 skipped), Redis (6 skipped, 1 pending).
tlsmodule was loaded, so no Tls case skipped.Merges cleanly with the pipe-adapter change opened alongside it (#399), with its follow-up (#401), and with #386, #384 and #363. Against #307 the only conflict is in
HardeningTests.cs, which #307 already has with main.API: none.
🤖 Generated with Claude Code