Repository navigation
Read path: empty runs cost nothing, InfiniSegment charged what it allocates, one ReadWords (#329) - #338
Merged
Conversation
…ocate (red) (#329) Two tests, one for each of #329's first two parts. TestAnEmptyRunIsTheEmptyArrayAndCostsNothing (TestPayloadReader) reads an empty run with PayloadReader.ReadBytes, from a held payload and from a stream, and asks for the runtime's empty array, charged, priced and allocated at nothing, counted in a no-GC region. A run of one byte is held the other way, to an array of its own at 32 bytes, so a cost of nothing for every run fails too. Red on 05a5669, at the first check: a run of 0 read from a held payload was charged 24 bytes Probed on 05a5669, every figure the test asks for: charged allocated empty array priced empty, held 24 0 yes 24 empty, from a stream 24 24 no 24 one byte, held 32 32 no 32 one byte, from a stream 32 32 no 32 TestASegmentIsChargedWhatRestoringItAllocates (TestInfiniFilter) holds InfiniFilter.SegmentCost, which the reader charges and the price prices for each table, to what InfiniSegment.Restore allocates for the same shape, counted in a no-GC region after one uncounted restore: at least that, and less than a small object (64 bytes) past it. Red on 05a5669, at the narrowest table: a segment of 2^2 slots of 2-bit fingerprints allocated 120 bytes to restore, and a read is charged 104 for it The table is 40 bytes of that, so the segment is 80 against the 64 of ReadBudget.SmallObject. Probed on 05a5669 at tables of 2^4, 2^10 and 2^16 slots, the segment beyond its table is 80 bytes at each. For the second test to compile, SegmentCost goes from private to internal here; nothing else in the library changes in this commit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KK3MspqAcczxT45xCfLgyx
PayloadReader.ReadBytes charged ReadBudget.ArrayOf(0, 1), an array's header of 24 bytes, for an empty run: from a held payload, where the span it copies out is the runtime's empty array and nothing is allocated, and from a stream, where it allocated new byte[0]. PriceBytes priced the same 24, so price and charge agreed on a cost the read did not have (held) or need (from a stream). Both now go through CostOfBytes, nothing for an empty run and the array otherwise, and from a stream an empty run is Array.Empty<byte>(). The held path is unchanged but for the charge. TestAnEmptyRunIsTheEmptyArray- AndCostsNothing passes: charged, priced and allocated nothing and the runtime's empty array from both, and a run of one byte 32 from both. Which charges move, measured by a probe (not committed) that read, at 05a5669 and here, every budget sample (TestReadBudget.Samples) and an empty instance of each structure in the empty round-trip sweep, held and from the large form, plus a TopK, a HeavyKeeper and a VarOpt holding an empty key or item: held charge large charge large alloc new DDSketch 264 -> 216 288 (same) 592 (same) DDSketch budget sample 480,072 -> 480,048 same same TopK, one empty key 436,640 -> 436,616 110,064 -> 110,040 109,904 -> 109,880 HeavyKeeper, one empty key 3,720 -> 3,696 2,448 -> 2,424 2,488 -> 2,464 VarOpt, one empty item 840 -> 816 856 -> 832 1,032 -> 1,008 24 bytes an empty run, everywhere one is read through ReadBytes, and nothing else moved: every other sample and empty structure is charged as it was. The DDSketch's large-form figures do not move because its runs there are begun by BeginWords, which reads from a stream in pieces and does not go through ReadBytes. TestReadsAllocateOnlyWhatTheyHold (C16's sweeps), TestReadBudget, TestPayloadReader, TestLargeEnvelope*, TestBuckets64 and TestCapacityDocumentation all pass with it: 379 of 379. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KK3MspqAcczxT45xCfLgyx
InfiniFilter.SegmentCost, which the reader charges and the price prices for each table, was the table's words and ReadBudget.SmallObject, 64 bytes, for the segment. An InfiniSegment declares four ints, five 64-bit fields and a reference, 64 bytes of fields and a 16-byte header, and measured in a no-GC region it is 80 bytes beyond its table at every shape probed (tables of 2^4, 2^10 and 2^16 slots, and 2^2 in the test): 16 bytes a table under what a read allocates, by construction, at every table. So the charge was not the upper bound it is meant to be, though the slack of every read-budget sample absorbed it, as #329 said it would. The segment is now charged as two small objects, 128 bytes, the bound NestedChecksumCost already uses for two objects. The reader and the price both call SegmentCost, so price and charge still come to one sum. TestASegmentIsChargedWhatRestoringItAllocates passes: at each shape the charge is at least what restoring the segment allocates, and 48 bytes past it, short of a small object. Each read of an InfiniFilter is charged 64 bytes more a table; no figure in the README or CAPACITY.md moves (TestCapacityDocumentation passes), and the charge line slopes are a table's words, as they were. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KK3MspqAcczxT45xCfLgyx
#329) InfiniFilter.ReadWords (#313) and CuckooBloomFilter.ReadWords (#230) were the same method: read as many words as a span holds straight into it, in pieces through a stack buffer of PayloadReader.BlockPieceFor (16 KiB) with ReadBlock. Both are replaced by PayloadReader.ReadWords(Span<ulong>), whose body is theirs, with no change in what is read, allocated or charged. InfiniFilter no longer needs System.Buffers.Binary. The overload, and the two crefs that name the existing ReadWords() and would otherwise be ambiguous with it (CS0419 under -warnaserror), are written exactly as #328's branch writes them, where the quotient filter's reader takes them up, so that the two branches add the same lines and merge either way round. TestInfiniFilter, the cuckoo tests, TestReadBudget, TestLargeEnvelope- Reading (its Infini pieces test among them), TestReadsAllocateOnlyWhat- TheyHold and TestPersistence*: 642 of 642 pass. Mutation of #329's three parts, scripts/mutate.sh, MUTATION_JOBS=2, at this commit's tree (5752769, before this table was added to its message), whole suite unless a test is named in brackets: (1) cost: empty run charged as an array killed (1) (1) cost: condition flipped killed (61) (1) cost: bound shifted to one byte killed (2) (1) stream: new byte[0] for an empty run killed (1) (1) stream: empty-array condition flipped killed (45) (1) held: charged as at 05a5669 killed (2) (1) stream: charged as at 05a5669 killed (1) (1) price: priced as at 05a5669 killed (2) (1) price: priced nothing killed (59) (2) segment: one small object (05a5669) killed (1) (2) segment: three small objects killed (1) (2) segment: no small object killed (1) (3) words: pieces of one word killed (1) (3) words: buffer not cut to the run SURVIVED (all 2,504) (3) words: last piece read whole killed (17) (3) words: last piece not read killed (67) (3) words: last word of a piece dropped killed (51) (3) words: read big-endian killed (61) (3) infini: table not read killed (43) (3) cuckoo: occupancy not read killed (47) [empty run] held charged as at 05a5669 killed (1) [empty run] stream new byte[0] killed (1) [segment] one small object (05a5669) killed (1) [segment] three small objects killed (1) [infini pieces] pieces of one word killed (1) (1) mutates CostOfBytes' condition (dropped, flipped, its bound moved to a run of one byte), the stream path's choice of the empty array (dropped, flipped), and each of the three places it is charged or priced, put back to ReadBudget.ArrayOf. The bound moved to one byte is what the test's run of one byte is for; the empty array dropped from the stream path allocates 24 bytes it is not charged for, and only the new test sees either that or the held charge put back ([empty run]). (2) mutates SegmentCost's count of small objects: one, as at 05a5669, fails only the new test, since the read-budget slack absorbs 16 bytes a table, which is why it went unnoticed; three fails its bound from above. (3) mutates the shared overload, now reached through Infini and the cuckoo filter, and drops each caller's read. The Infini pieces test alone kills one-word pieces ([infini pieces]), so the overload is held there as #313 held its own copy. The survivor is the one #289 and #313 adjudicated: the loop cuts every piece to Math.Min(left, piece.Length), which is Math.Min(left, BlockPieceBytes) whether or not the buffer was cut to the run, since left never exceeds the run, so the same bytes are read in the same calls; only stack use differs, which no test sees. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KK3MspqAcczxT45xCfLgyx
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KK3MspqAcczxT45xCfLgyx
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.
Part of #329: parts (1)–(3). Part (4) is in #333.
These are three read-path leftovers, per the coordinator's plan on #329, approved by Matt.
(1) An empty run costs nothing
ReadBytesnow returnsArray.Emptyfor an empty run, whether the payload is held or streamed, and charges nothing.PriceBytesmatches.Charges that moved (measured with a probe that reads every budget sample, an empty instance of each structure, and empty-key cases):
DDSketch, held: a new sketch 264 → 216 bytes; the budget sample 480,072 → 480,048.TopK,HeavyKeeperandVarOptholding an empty key or item: 24 bytes less for each empty run, in both forms, and 24 bytes less allocated from the large form.C16's sweeps,
TestReadBudgetandTestCapacityDocumentationall stay green.Red:
TestAnEmptyRunIsTheEmptyArrayAndCostsNothingfailed with "a run of 0 read from a held payload was charged 24 bytes". It also holds a one-byte run to 32 bytes, so a cost that is always zero fails too.(2) The
InfiniSegmentundercharge was realMeasured: a segment is 80 bytes beyond its table at every shape tried, against
SmallObject's 64.SegmentCostnow charges it as two small objects.Red:
TestASegmentIsChargedWhatRestoringItAllocatesfailed with "allocated 120 bytes to restore, and a read is charged 104". Only this test catches it, because the read budget's slack hid it. No documented figure moved.(3) One
ReadWordsInfini and Cuckoo now both call
PayloadReader.ReadWords(Span<ulong>), with no change in behaviour. The overload is byte-identical to the one in #328's PR, and a trial merge both ways was clean.Mutation
24 killed and 1 survivor, the same equivalent buffer cut as in #289. The table is in
f9736ad's message.Verification
-warnaserror: exit 0.🤖 Generated with Claude Code
https://claude.ai/code/session_01KK3MspqAcczxT45xCfLgyx