Skip to content

Overlap the two reads when comparing binary snapshots - #1889

Merged
SimonCropp merged 2 commits into
mainfrom
perf-stream-compare
Aug 29, 2026
Merged

Overlap the two reads when comparing binary snapshots#1889
SimonCropp merged 2 commits into
mainfrom
perf-stream-compare

Conversation

@SimonCropp

Copy link
Copy Markdown
Member

The verified and received streams were filled strictly one after the other, in 8KB chunks, so a passing run read both files end to end sequentially for every binary snapshot.

The two streams are independent, so the fills now overlap, which roughly halves the wall time. The chunk is 64KB, still under the large object heap threshold and inside the array pool's buckets, so there are 8x fewer round trips. On net6.0 and above the Memory overload of ReadAsync avoids the Task allocation the byte[] overload makes per call against a FileStream opened for async IO.

The equal sized chunk alignment the length comparison depends on is unchanged, and WhenAll rather than sequential awaits so a failure on one side cannot leave the other unobserved. docs/comparer.md is regenerated from the snippet.

The verified and received streams were filled strictly one after the other, in
8KB chunks, so a passing run read both files end to end sequentially for every
binary snapshot.

The two streams are independent, so the fills now overlap, which roughly halves
the wall time. The chunk is 64KB, still under the large object heap threshold and
inside the array pool's buckets, so there are 8x fewer round trips. On net6.0 and
above the Memory overload of ReadAsync avoids the Task allocation the byte[]
overload makes per call against a FileStream opened for async IO.

The equal sized chunk alignment the length comparison depends on is unchanged,
and WhenAll rather than sequential awaits so a failure on one side cannot leave
the other unobserved. docs/comparer.md is regenerated from the snippet.
@SimonCropp SimonCropp added this to the 33.0.0 milestone Aug 29, 2026
StreamComparerBenchmarks measures the previous commit against the implementation
it replaced, kept in LegacyStreamComparer with the buffer size and read overload
lifted to parameters. The four rungs isolate each part of the change: 8KB
sequential byte[] reads, then 64KB, then the Memory overload, then the shipped
overlapped version. Both sides are real async FileStreams opened the way
IoHelpers.OpenRead opens them, and each size carries its own baseline so the
ratios compare like for like.

The 1MB compare drops to 0.13 of the old time and a ninth of the allocation, and
64KB to 0.26. At 2KB the buffer size does nothing, one read either way, so the
whole 0.77 there is the overlapping. A mismatch that exits on the first chunk is
0.51.

The buffered category is a MemoryStream against a FileStream, not a MemoryStream
against another one. InnerCompare always opens the verified side with
IoHelpers.OpenRead, so a pair of MemoryStreams never reaches the comparer, and
measuring that shape reported a regression that cannot occur. Against the real
shape it is 0.36.

MixedEqualSpanningMultipleBuffers covers that buffered shape, which had only
not-equal coverage. EqualWithShortReads covers the accumulation loop in
ReadBufferAsync: a local FileStream returns the full buffer every time, so
replacing that loop with a single read leaves every other test passing. A stream
that always reads short keeps both sides chunk aligned under test, where a desync
would surface as a spurious NotEqual on a network share.
@SimonCropp
SimonCropp merged commit bd66bfa into main Aug 29, 2026
3 of 5 checks passed
@SimonCropp
SimonCropp deleted the perf-stream-compare branch August 29, 2026 13:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant