Repository navigation
QuotientFilter read leftovers, and a byte left over refused as malformed under its own price (#343) - #348
Merged
Conversation
…lter and its table (red) #343 part (1): QuotientFilter.Build makes the filter a read fills through the public constructor, new QuotientFilter(1, 0.5, hash), whose Shape allocates a two-word table, and then shapes the filter again to the payload's widths, so every read allocates a 40-byte table it throws away. The test holds Build to exactly the table shaped to the payload plus what the constructor allocates beyond its own table, the filter object. That figure is measured in the test, at (1, 0.5) and (50,000, 0.001), rather than written down, and the two shapes have to agree and be more than nothing, so the comparison cannot pass by holding Build to its table alone. The slots each table should have are worked out from the widths and checked against the filter built. The shapes are #341's: the narrowest table, the widest remainder, 2^8 and 2^16 slots, and version 1's one-slot table for 32 quotient bits. Every count is in a no-GC region, after an uncounted warm-up. Red, on 6e2e5ec, Linux x64: a filter of 2 slots of 1-bit remainders allocated 176 bytes to build for a read: 136 beyond its table of 40, where the filter the constructor makes is 96 The guards passed: the constructor allocated 96 beyond its table at both shapes, and every filter built had the slots its widths give. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KK3MspqAcczxT45xCfLgyx
…e it two small objects #343 part (1). Build made the filter a read fills through the public constructor, new QuotientFilter(1, 0.5, hash), and then shaped it again to the payload's widths, so every read allocated the one-item filter's two-word table, 40 bytes, and threw it away. Build now calls a private constructor that shapes the filter at the widths it is given and sets the hash, and nothing else, so the table is made once. Beyond the table, building it now allocates the filter object alone, 96 bytes, so TableCost charges it as two small objects, 128, where #336 charged three for the 136 the old Build allocated: 32 past what is allocated, under one small object, as the InfiniFilter and MementoFilter segments are charged (#329, #327). The reader and its price both call TableCost, so they still come to the same sum. #341's TestAReadIsChargedWhatBuildingItsFilterAllocates keeps its bounds; its summary and remarks now say what Build allocates. Measured on this commit, Linux x64, in no-GC regions: Build, 400 repeats at each shape, every repeat the same, on the regions collector and on segments (DOTNET_GCName=libclrgc.so, three processors, the test host reporting GCName=libclrgc.so and GCRegionRange=0): 2 slots of 1 bit 136 (table 40 + 96) 2^16 slots of 16 bits 155,784 (table 155,688 + 96) version 1's one slot 136 (table 40 + 96) A whole read, least of seven, allocated minus charged, the same at four shapes from (1, 0.5) to (1,000,000, 0.0001) and in both envelopes: read from a stream, its budget made outside the count -72 (was -96) FromByteArray under ReadOptions.Unlimited +40 (was +16) Each read allocates 40 bytes less and is charged 64 less. The +40 is not the filter's: see #343 part (2) in the report. Every structure's FromByteArray read allocates a 64-byte MemoryStream and its 48-byte ReadBudget, which no charge names, and the envelope charges its 24-byte Crc32 as a 64-byte small object; 112 - 40 - 32 = 40. Watched in the middle: with Build fixed and TableCost still at three small objects, the new test passed and #341's failed its upper bound, as the issue said it would: a filter of 2 slots of 1-bit remainders is charged 232 bytes, 96 past the 136 building it allocated TestBuildingTheFilterAReadMakesAllocatesOnlyTheFilterAndItsTable, red on the parent, passes, with the rest of TestQuotientFilter. Mutation, through scripts/mutate.sh with MUTATION_JOBS=2, on this commit: the full suite for each, then each scoped to the new test (NEW) and to #341's TestAReadIsChargedWhatBuildingItsFilterAllocates (OLD): F1 the private constructor shapes a two-word table first, as the old Build did killed (2 tests: NEW and OLD) F2 the private constructor sets no hash killed (32 tests) F3 TableCost three small objects killed (1 test: OLD; NEW survives) F4 TableCost one small object killed (1 test: OLD; NEW survives) NEW holds what Build allocates and never reads TableCost, so F3 and F4 survive it by design; OLD holds the charge to the build from both sides, F3 past its upper bound and F4 under its lower one. F1 fails OLD's lower bound as well as NEW: 176 allocated against 168 charged at two slots. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KK3MspqAcczxT45xCfLgyx
…ay as its reader does #343 part (3). QuotientFilter's price was mutated on its own, on 6eb7e4b's tree (a137724, the same tree before its message was amended), through scripts/mutate.sh with MUTATION_JOBS=2 and the full suite for each: P1 the price begins the table's run with BeginRun, which holds it to one array, not BeginWordsInPlace SURVIVED (all 2525 passed) P2 the price skips no run killed (3 tests) P3 the price checks no end SURVIVED (all 2525 passed) P4 the price checks the end after pricing the table SURVIVED (all 2525 passed) P5 the price reads the count as a u64 at every version SURVIVED (all 2525 passed) P6 the price reads the count as a u32 at every version killed (5 tests) P7 the price checks no widths killed (4 tests) P8 the price never takes version 1's one-slot table killed (4 tests) P9 the price prices no table killed (3 tests) P10 the price prices a remainder a bit wider killed (4 tests) P11 the price reads the two widths swapped killed (3 tests) P1 is the gap #335 found in the other three structures. No payload this library writes in the suite holds a run past one array, and a refused price is set aside: the read is admitted at what was priced by then, and its reader then charges the table, so a budget too small for the whole is refused by the reader instead, naming the same total. This commit kills P1. P5 is the next commit's. P3 and P4 are not test gaps and are not killed: see the report for #343. With either, a payload holding a byte past its table is refused for that byte under its own price, as it should be, where this tree refuses it for its budget. The test is #342's TestAPriceTakesARunOfWordsPastOneArrayAsItsReaderDoes with a fourth row: a quotient filter of 2^31 slots of 5-bit remainders, a table the constructor builds, whose 2^28 + 2 words are 2,147,483,664 bytes, past one array, written in the large form at version 12. Its fields are where FORMAT.md lays them out, the two widths, the count and the run's u64 length, on a stream of zeros that claims them all. The price is walked on its own and must price exactly TableCost, what the reader charges for the table and the filter. Passes here. Mutation, through scripts/mutate.sh with MUTATION_JOBS=2, on this commit: P1 scoped to TestAPriceTakesARunOfWordsPastOneArrayAsItsReaderDoes killed (1 test) P1 against the full suite killed (1 test) Hand-applied, the QuotientFilter row is the one that fails, and for the reason it names, while the other three rows pass: System.IO.InvalidDataException: Payload holds a 2147483664-byte run, which cannot be read into a single array. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KK3MspqAcczxT45xCfLgyx
…t its reader charges #343 part (3), the price's other survivor. Mutated on its own on 6eb7e4b's tree (the table is in the parent's message), P5, a price that reads the count as a u64 at every version, passed the whole suite: 2525 tests. A payload of version 1 counts in a u32, so that price reads the run's length into the count and the table's first bytes as the run's length, and refuses the payload; a refused price is set aside, and the read is admitted at the envelope alone, its reader then charging the table. So a version 1 payload read whole still loads, and only a refusal's number shows the difference. The test takes the three stored fixtures (version 1's one-slot table, quotientfilter-v1.bin and quotientfilter-v12.bin) and a filter written now, and for each walks the price on its own, which must not refuse the payload and must price exactly TableCost, what the reader charges; then reads it whole, refused under a budget of nothing naming the envelope and that, loaded under that total as the filter it reads as under no limit, and refused under a byte less. The versions the payloads cover are held to PersistenceFormat.ReadableVersions, so a version added without a payload here fails. Passes here. Hand-applied P5, the test fails at the one-slot table, its first payload: one slot, at version 1: its price refused it, so it is set aside and the read admitted at less: Payload ended after 32 bytes while reading 8065745 more at offset 20. ... and with the price's walk taken out of the test, so only the whole read is checked, it fails on that too: one slot, at version 1, refused under a budget of nothing, named the wrong total (expected 360, actual 192) Mutation, through scripts/mutate.sh with MUTATION_JOBS=2, on this commit: P5 scoped to this test killed (1 test) P5 against the full suite killed (1 test) P3 the price checks no end SURVIVED (all 2527 passed) P4 the price checks the end after pricing the table SURVIVED (all 2527 passed) P3 and P4 are left alive on purpose, and are not equivalent: each is the price this tree should have. A payload holding a byte past its table is refused for its budget under its own price, because the price checks the end before it prices the table, and its reader charges the table before it checks the end. Measured on this tree's library, a filter for (1, 0.5) holding one key, one byte added and the checksum resealed: as committed admitted at 200, the envelope alone; under 200 refused for its budget, needing 368; under 368 refused for the byte left over P3 or P4 admitted at 368; under 368 refused for the byte left over and the same at (50,000, 0.001): admitted at 213,192 and refused needing 426,352, or, with either mutant, admitted at 426,352. A test that killed P3 or P4 would pin the refusal for the budget. It is the defect InvertibleBloomLookupTable's price had (TESTING.md, "A price prices each array where its reader charges it"). It is left for a Decision, not fixed here. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KK3MspqAcczxT45xCfLgyx
…nder its own price (red) #343, found by mutating the price on its own (P3 and P4 in e59fb71's message). QuotientFilter.Price checks that the payload ends after the table before it prices the table, and its reader charges the table before it checks the end. So a payload holding a byte past its table is priced at the envelope alone: the price stops at the end check, it is set aside, the read is admitted at what it had priced, and the reader charges the table and is refused for a budget its own price fits, rather than for the byte. It is the defect InvertibleBloomLookupTable's price had (TESTING.md, "A price prices each array where its reader charges it"). The test is modelled on TestAnInvertibleTableWithAByteLeftOverIsRefusedForItUnderItsOwnPrice, in both envelopes. The smallest filter the constructor builds, holding one key, is written in each, one byte is added after the payload and the length and checksums made to match (WithAByteLeftOver, which reseals the large form's header checksum as well as its end checksum). The price is walked on its own over the longer payload and must stop at the byte, for it; the read is then read under exactly the envelope and what the walk priced, and must be refused for the byte, not for its budget; and under a byte less it is refused for its budget naming that price, so the price worked out here is the one the read is admitted at. Red on e59fb71, Linux x64, in each envelope (the large form run on its own, since the loop stops at the first): in the ordinary envelope, under exactly its own price of 200, it was refused for its budget rather than for the byte left over: ... new ReadOptions(368) in the large form, under exactly its own price of 240, it was refused for its budget rather than for the byte left over: ... new ReadOptions(408) The premises held in both: the price stopped at the byte, for it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KK3MspqAcczxT45xCfLgyx
…hecking the end #343. QuotientFilter.Price checked that the payload ended after the table before it priced the table, and the reader charges the table before it checks the end, so a payload holding a byte past its table was priced at the envelope alone and its read refused for a budget its price fits. The price now checks the end last, after it has priced the table, so it prices the table where the reader charges it: once the table's run is held and the widths are checked. A payload with nothing left over is priced as it was, at every step. Measured on this commit, a filter for (1, 0.5) holding one key, one byte added and its checksums resealed: admitted at 368 bytes in the ordinary envelope (was 200) and 408 in the large form (was 240), and under each refused for the byte left over, not for its budget, and under a byte less refused for its budget naming that price. TestAFilterWithAByteLeftOverIsRefusedForItUnderItsOwnPrice, red on the parent, passes, with the rest of TestQuotientFilter*. Mutation, through scripts/mutate.sh with MUTATION_JOBS=2, on this commit: the price on its own again, the full suite for each, and the reorders also scoped to the new test, which is the one test each is killed by: R1 the end checked after the widths and before the price killed (1 test; the new one) R2 the old order exactly, the end checked before the widths and the price killed (1 test; the new one) R3 the price checks no end killed (1 test; the new one) P1 the run held to one array killed (1 test) P2 no run skipped killed (3 tests) P5 the count a u64 at every version killed (1 test) P6 the count a u32 at every version killed (6 tests) P7 no widths checked killed (4 tests) P8 version 1's table never wrapped killed (5 tests) P9 no table priced killed (6 tests) P10 a remainder a bit wider killed (6 tests) P11 the widths read swapped killed (5 tests) R2 is the parent's price and R1 is the same fault one statement later; both are killed by the read under its own price being refused for its budget. R3, hand-applied, is killed by the test's premise only ("the price stops for the byte left over"): with no end check the price walks a payload with a byte left over to its end and prices all of it, and the read is then refused for the byte under that price, as it is with this commit. At the level of a read R3 is the same; the premise holds the price to refusing what its reader refuses, as the invertible table's test does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KK3MspqAcczxT45xCfLgyx
One release ships both, so the one bullet now says what that release does, measured against the last one, 6.2.0, which had no read budget: the reader builds the filter at the payload's widths with no table to throw away, and charges it as two small objects beside its table. The intermediate states, charged as one small object and then as three, never shipped and are gone. The price fix keeps its own bullet below. 6.2.0's figure was measured from its tree (git archive v6.2.0, built in a scratch directory, outside this repository), on the same runtime, 10.0.14: the least of seven calls of its private Build in a no-GC region, by reflection, beyond the table each built: 2 slots of 1 bit 160 allocated, table 40 120 beyond 4 slots of 61 bits 192, table 72 120 2^8 slots of 7 bits 480, table 360 120 2^16 slots of 16 bits 155,808, table 155,688 120 and new QuotientFilter(1, 0.5) alone, whose two-word table Build threw away, 120: its filter object then, 80, and the table, 40. On this branch Build allocates 96 beyond its table at the same tables and at version 1's one-slot table (6eb7e4b's message). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KK3MspqAcczxT45xCfLgyx
…is refused for it under its own price (red) #343. Probing every structure for the quotient filter's price defect (its price checked the payload's end before pricing its table, where its reader charges the table first) found one more: CountSketch. Its price checks the end before it prices its rows, and its reader charges the rows before it checks the end, so a payload holding a byte past its cells is admitted at the envelope alone and refused for a budget its own price fits. Three changes, all tests: - TestBytesLeftOver, a new file. WithAByteLeftOver (moved here from TestQuotientFilter) adds one byte after a payload in either envelope and reseals its length and checksums. AssertRefusedForTheByteUnderItsOwnPrice is the quotient filter's test made general: for a structure whose price checks the end, the price walked alone stops at the byte, for it; the read under exactly the envelope and what the walk priced is refused for the byte, not for its budget; and under a byte less it is refused for its budget, naming that price. And the sweep, TestEveryStructureWithAByteLeftOverIsRefusedForItUnderItsOwnPrice, whose roster is StructureId through TestReadBudget.Structures: every structure's read-budget sample, in each envelope, with a byte added, must be refused for the byte with no limit (so the byte is a fault), refused for its budget under a byte less than the total a refusal under nothing names, naming it (so that total is the price the read is admitted at), and refused for the byte under exactly that price. The sweep finds the price from the read and does not walk it: most prices do not walk to their payload's end (probed, 50 of 66 structure-and-envelope rows priced to their end or stopped at the fields they price from without checking it), and the reader checks the end. - TestCountSketch.TestASketchWithAByteLeftOverIsRefusedForItUnderItsOwnPrice, the sketch at ε = δ = 0.5 holding one item, in both envelopes, through AssertRefusedForTheByteUnderItsOwnPrice. - TestQuotientFilter's byte-left-over test now goes through the same assertion, unchanged in what it asserts. Red on bf6873c, Linux x64: the sweep, CountSketch's row (32 other rows pass): CountSketch with a byte left over, under exactly its own price of 240,176, was refused for its budget rather than for the byte measured by the probe on the same library: in the large form, admitted at 240 and refused needing 240,360; in the ordinary envelope needing 480,296. the count sketch's own test: in the ordinary envelope, under exactly its own price of 208, the CountSketch was refused for its budget rather than for the byte left over: ... new ReadOptions(296) in the large form (run first, on its own): under exactly its own price of 240 ... new ReadOptions(328) The quotient filter's test passes, as it did. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KK3MspqAcczxT45xCfLgyx
…cking the end #343. CountSketch.Price checked that the payload ended after the cells before it priced the rows, and the reader charges the rows before it checks the end, so a payload holding a byte past its cells was priced at the envelope alone and its read refused for a budget its price fits: the quotient filter's defect, fixed the same way. The price now checks the end last, after it has priced the rows, so it prices them where the reader charges them: once the cells are held and their shape checked. A payload with nothing left over is priced as it was. The CHANGELOG's bullet for the quotient filter's price fix now covers both. TestASketchWithAByteLeftOverIsRefusedForItUnderItsOwnPrice and the sweep's CountSketch row, red on the parent, pass: TestBytesLeftOver, TestCountSketch, TestQuotientFilter* and #342's price test, 148 of 148. Mutation, through scripts/mutate.sh with MUTATION_JOBS=2, on this commit: CountSketch's price on its own, the full suite for each, CR1 the end checked after the cells and before the price killed (2 tests) CR2 the old order exactly killed (2 tests) CR3 the price checks no end killed (1 test) C1 the run held to one array killed (1 test) C2 no run skipped killed (2 tests) C3 no cells checked killed (4 tests) C4 no rows priced killed (5 tests) C5 a row more priced killed (6 tests) C6 width and depth read swapped killed (4 tests) and scoped, which test kills the reorders: CR1 and CR2 are each killed by the sketch's own test and by the sweep; CR3 by the sketch's own test, and survives the sweep (all 33 passed). With no end check the price walks a payload with a byte left over to its end and prices all of it, and the read is refused for the byte under that price, as it is with this commit: CR3 is the same at the level of a read, which is all the sweep looks at, and the sketch's own test holds the price to refusing what its reader refuses. The sweep, red on each of the two price fixes reverted, one at a time: QR2 QuotientFilter's old order exactly, against the full suite killed (2 tests); against the sweep alone killed (1 test). Hand-applied, the sweep fails on QuotientFilter's row only, 32 rows passing: QuotientFilter with a byte left over, under exactly its own price of 213,192, was refused for its budget rather than for the byte CR2 CountSketch's old order exactly, against the sweep alone killed (1 test). Hand-applied (the end checked before the price), the sweep fails on CountSketch's row only, 32 rows passing: CountSketch with a byte left over, under exactly its own price of 240,176, was refused for its budget rather than for the byte Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KK3MspqAcczxT45xCfLgyx
This was referenced Nov 6, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KK3MspqAcczxT45xCfLgyx
mattlorimor
added a commit
that referenced
this pull request
Nov 6, 2026
Brings in #345 and #348 (2a4dfbc). CHANGELOG merged without conflict. #345's TestANestedEnvelopeClaimingMoreThanItsRunIsRefusedAsData fails on this merge, as expected: it pins Slice's refusal, and the price now refuses first, in RequireHeld. The commit after this holds it to that. 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.
Fixes #343.
All three of #343's
QuotientFilterread leftovers are dealt with, following the coordinator's plan on #343, approved by Matt.QuotientFilter, and a new every-structure test found the same one inCountSketch. Both are fixed here, under C20's rule that a price prices each array where its reader charges it.Part 1: no throwaway table
Buildused to go throughnew QuotientFilter(1, 0.5, hash), which allocates a two-word table, and then the read replaced it.Buildnow calls a private constructor that shapes the filter at the payload's widths directly.TableCostdrops from three small objects to two: the table plus 128 bytes. The price and the reader both use it, so they still agree.Measured:
Buildallocates the table plus 96 bytes, the same in 400 of 400 repeats per shape, under both collectors. A whole read now allocates 40 bytes less and is charged 64 less.Test:
TestBuildingTheFilterAReadMakesAllocatesOnlyTheFilterAndItsTable.Buildmust allocate exactly its table, plus what the constructor allocates beyond its own table.Buildto its table alone.6e2e5ec: "a filter of 2 slots of 1-bit remainders allocated 176 bytes to build for a read: 136 beyond its table of 40, where the filter the constructor makes is 96".#341's test keeps its bounds; only its comments changed. Those bounds are what force the charge down: with
Buildfixed but the charge still at three small objects, it failed as #343 predicted, "charged 232 bytes, 96 past the 136".Mutation: the throwaway table back, no hash set, three small objects and one small object. All four killed.
Part 2: the 72 bytes come from the envelope, so this stops here
Measured for every
StructureId, in both envelopes:FromByteArrayread allocates 112 bytes nothing charges: theMemoryStream(64) and the read's ownReadBudget(48).Crc32(24 bytes) as a 64-byte small object, 40 over.QuotientFilter's 72 was 112 − 40.No shared code changes in this PR. Matt has decided that every read must be charged at least what it allocates, and #346 takes the envelope's share and the per-structure gaps.
Part 3: the price on its own
QuotientFilter.Pricewas mutated on its own: 11 mutants, 4 survived.QuotientFilterrow in Read CountSketch, IBLT and DDSketch runs of words straight into storage (#335) #342'sTestAPriceTakesARunOfWordsPastOneArrayAsItsReaderDoes: 2^31 slots of 5-bit remainders, a 2,147,483,664-byte table.u64at every version: no test priced a version-1 payload. The newTestAPayloadOfEveryVersionIsPricedAtWhatItsReaderChargesuses the three stored fixtures plus a filter written now. Its versions are held toPersistenceFormat.ReadableVersions.The byte-left-over fix, for
QuotientFilterandCountSketchThe fix: each price now checks the end after pricing the table, or the rows, where its reader charges them. A payload with nothing left over is priced as it was.
TestBytesLeftOver.cs, a new sweep with its roster fromStructureId, in both envelopes. Every structure's sample, with one byte added and the checksum resealed, must be refused for that byte under exactly its own price.bf6873c: onlyCountSketch's rows fail, "under exactly its own price of 240,176, was refused for its budget".Per-structure tests:
TestQuotientFilter.TestAFilterWithAByteLeftOverIsRefusedForItUnderItsOwnPrice. Red: in the ordinary envelope, admitted at 200 and refused for its budget, needing 368.TestCountSketch.TestASketchWithAByteLeftOverIsRefusedForItUnderItsOwnPrice. Red: admitted at 208, needing 296.These also hold each price to stopping at the byte, which the sweep can't check, because most prices don't walk to their payload's end.
Mutation:
QuotientFilter.Priceafter the fix: 15 killed, 0 survived.CountSketch.Price: 17 killed, 1 survived. The survivor is "the price checks no end". It behaves exactly like the fix at read level, and it's killed by the sketch's own test.CHANGELOG
QuotientFiltercharge bullet and this PR's are folded into one, describing the final state against 6.2.0. 6.2.0's figures were measured on a build ofv6.2.0.Verification
-warnaserror: exit 0.Left for later, now #346
CountSketch's reader starts fromnew CountSketch(0.5, 0.5)and then replaces its table, the throwaway this PR removes fromQuotientFilter.🤖 Generated with Claude Code
https://claude.ai/code/session_01KK3MspqAcczxT45xCfLgyx