Repository navigation
perf(tree): decode leaf records on demand so the catalog fits in the node cache - #90
Merged
Merged
Conversation
Decoding a leaf used to work out the encoded size of every key and value straight away, for inline member lists inside catalog records too. Only writes read those sizes. A frozen leaf now measures itself the first time bytes() or heapBytes() is called, and writable leaves still measure when they are created.
Codecs that write each value without reference to the others (the catalog, posting lists and row codecs) now say so with independentValues(), and their leaf pages put the length of every value in front of it. A reader can then find one record without decoding the ones before it. Entry sizes count the length, so leaves never overflow a page. Storage format goes to 8. The history compaction test sat right at the 50% live ratio, and one byte per record pushed its early segment over, so it now compacts below 75%.
Loading a catalog or posting-list leaf used to decode every record on the page, with every inline member list, to answer a lookup for one of them. A leaf whose records are framed by length now keeps the page bytes and the offset of each record, and decodes a record on first read. The decoded record is kept in the leaf, published with a compare-and-set so readers on other threads see it whole, and charged to the node cache entry that holds the leaf, so the cache still evicts to its budget as records unpack. Charges for an entry that was evicted or cleared are dropped. A packed leaf costs its page image plus its keys and offsets until records are read, so far more of the catalog fits in a given node cache.
Decoding a leaf compared the header's refers-to-pages flag with its values, which a packed leaf can't do without decoding every record. The packed leaf now keeps the flag, and TreeVerifier, which decodes every record anyway, compares the two, so hstore check still catches a header that disagrees with its contents.
A writer copying a packed leaf, or measuring it, read every record through value(), which kept each decoded record in the frozen leaf and charged the node cache for it, one shard lock per record, for a leaf the write was about to replace. Both now decode records straight into the copy or the measurement and leave the packed leaf as it was.
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.
Why
At scale 4 with 64 MiB caches, HyperGraphDB enumerated a hyperedge's members about 8 times faster than HStore (0.13x), and two-hop neighbourhoods about 5 times faster (0.19x). At scale 1, HStore led on both.
Profiling the scale 4 case showed where the time went:
NodeCodec.decodedecoded all of the leaf's ~83 records, each with its inline member list, to return one record. That decode accounted for 70% of the time spent reading members.Smaller pages don't fix it. With 4 or 8 KiB pages, a lookup visits about 10 pages instead of 3.
What changes
ValueCodec.independentValues(): the catalog (AtomCodec), posting directories (PostingsCodec) andValueCodec.rows. In their leaves,NodeCodecwrites each value's length in front of it. This costs about one byte per record, and the leaf budget counts it.TreeVerifier(hstore check) now does that comparison instead.Numbers
Probe runs at scale 4, alternating main and this branch, two runs each. The miss counts are exact; throughput moved by up to 3x between runs on this machine.
Scale 1 is unchanged: 2.3M against 2.4M member lookups per second, and 230k against 224k two-hop queries per second.
The comparison harness at scale 4 (
async, 64 MiB caches), with 3 HStore runs on this branch, set against the main and HyperGraphDB runs from the benchmark report (3 runs each, earlier the same day):read.membersread.twohopread.incidence.coldread.incidenceAt scale 1, main and this branch ran back to back, alternating, 3 runs each. No workload moved by more than the spread between runs, except two:
read.incidence.parallelis 1.28x faster on this branch;read.memberscame out at 0.80x by median, but this branch's runs (2.59M, 2.35M and 1.48M per second) overlap main's (3.06M, 2.31M and 2.93M). Runs on this laptop vary that much. I'd still rerun scale 1 on a quiet machine before updating the published numbers.Testing
PackedLeafTest:hstore checkcatches a header whose page flag disagrees with the records.hstore checkverifies the trees and reference counts.COMPACTandcheckpass afterwards.