From d61c7ca40745b281e8d3cbaaa985d24cefcc00fa Mon Sep 17 00:00:00 2001 From: Christopher Haster Date: Sun, 25 Feb 2024 22:50:52 -0600 Subject: [PATCH] Improved rbyd compaction algorithm, reduced attr estimate 3t+4 -> (5/2)t+2 The motivation for this is that the rbyd inner node encoding during compaction is kind-of not that great. Our alt encoding is great when the trunk terminates in a tag, which is how it was originally designed to be used: 00000004: data w1 1 61 a <. 00000020: altble 0x300 w1 0x4 -' <-- trunk 00000024: data w1 1 62 b But when used to create an arbitrary binary-tree inner node, the best encoding I can think of is 2 alts + a terminating null tag, which is not that great: 00000004: data w1 1 61 a <. 00000009: data w1 1 62 b <--. 0000000e: altble 0x300 w1 0x4 -' | <-- trunk 00000012: altble 0x300 w1 0x9 ---' 00000016: null This effects our attr estimate, which is defined as the worst-case on-disk cost of an attr after compaction. This is an important value, as it determines when we split rbyds. And it effectively determines how densely we can store metadata without needing to worry about block overflow issues. In our current compaction algorithm, we connect each attr with a 2 alt + null inner node. Since we are creating a perfectly balanced binary tree, this works out to ~1 inner node per attr. Including the attr's data tag, this gives us: a_0 = 3t + 4 Where t is the tag estimate, currently a 2 byte tag, <=5 byte weight, <=4 byte size, t = 2+5+4 = 11 bytes: a_0 = 3*11 + 4 = 37 bytes Though this may be vary across different littlefs configurations, 16-bit, 64-bit, etc. --- It would be great if our compaction algorithm could build each trunk perfectly, as each attr is written. In such a case, each attr theoretically only needs ~1 alt and the attr's data tag: a_inf = 2t Or, assuming t = 11 bytes: a_inf = 2*11 = 22 bytes Unfortunately, as far as I can tell, this fundamentally requires unbounded RAM. You need to keep track of log n previous trunks in order to always build the next trunk perfectly, and log n is > 1. I suppose in theory you could implement a O(n^2) algorithm that repeatedly scans for the previous trunks... But that would be a hilarious regression since the whole point of this work was to reduce compaction from O(n^2) -> O(log n). --- However, we can meet halfway. Consider what happens if we build perfect trunks for only the bottom layer of the rbyd. This may not seem like it will gain much, but remember that in a binary tree, the bottom layer contains ~1/2 of the total nodes in the tree: 3t + 4 2t 5t a_1 = ------ + -- = -- + 2 2 2 2 Or, assuming t = 11 bytes: 5*11 a_1 = ---- + 2 = 30 bytes 2 Not too shabby for a constant amount of RAM. In theory this could be extended to n layers, by keeping a (configurable?) array of previous trunks in RAM during compaction, but this would have diminishing results as we move up the tree. With only needing to keep track of one other trunk, we can even store this in rybd.trunk, which is currently unused during compaction. So zero extra RAM. The resulting compaction looks like the following: 00000004: data w1 1 61 a <. 00000020: altble 0x300 w1 0x4 <--. 00000024: data w1 1 62 b | 00000029: data w1 1 63 c <. | 0000002e: altble 0x300 w1 0x29 <. | 00000032: data w1 1 64 d | | 00000037: altble 0x300 w2 0x20 -' | <-- trunk 0000003b: altble 0x300 w2 0x2e ---' 0000003f: null This does make compaction a bit more complicated, which is reflected in the code size: code stack before: 33856 2880 after: 34068 (+0.6%) 2880 (+0.0%) Some implementation things to note: - Since lfsr_rbyd_appendcompactattr now has state, we need to make sure to zero the trunk before compacting, which adds an annoying bit of bookkeeping everywhere. - I didn't want to add extra state to manage in lfsr_rbyd_appendcompactattr calls, so this implementation only tracks the previous trunk offset, and needs a readtag call to get the tag+weight necessary to build the actual alt pointer. This may have some unpleasant interactions with the rcache, and may be worth revisiting. --- lfs.c | 112 +++++++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 104 insertions(+), 8 deletions(-) diff --git a/lfs.c b/lfs.c index c24d57a8..2ac8aab7 100644 --- a/lfs.c +++ b/lfs.c @@ -3360,11 +3360,27 @@ static int lfsr_rbyd_commit(lfs_t *lfs, // determine the upper-bound cost of a single rbyd attr after compaction // -// note that with rebalancing during compaction, we know the number -// of inner nodes is roughly the same as the number of tags. Each node -// has two alts and is terminated by a 4-byte null tag. +// Note that with rebalancing during compaction, we know the number +// of inner nodes is roughly the same as the number of tags. Unfortunately, +// our inner node encoding is rather poor, requiring 2 alts and terminating +// with a 4-byte null tag: // -#define LFSR_ATTR_ESTIMATE (3*LFSR_TAG_DSIZE + 4) +// a_0 = 3t + 4 +// +// If we could build each trunk perfectly, we could get this down to only +// 1 alt per tag. But this would require unbounded RAM: +// +// a_inf = 2t +// +// However, we can meet halfway. The bottom layer in our rbyd contains 1/2 +// of all inner nodes, so if we build the bottom layer perfectly, we can +// reduce the attr estimate a bit without unbounded RAM: +// +// 3t + 4 2t 5t +// a_1 = ------ + -- = -- + 2 +// 2 2 2 +// +#define LFSR_ATTR_ESTIMATE ((5*LFSR_TAG_DSIZE+2-1)/2 + 2) // Calculate the maximum possible disk usage required by this rbyd after // compaction. This uses a conservative estimate so the actual on-disk cost @@ -3470,11 +3486,51 @@ static int lfsr_rbyd_appendcompactattr(lfs_t *lfs, lfsr_rbyd_t *rbyd, } } + // we eagerly connecting pairs in the first layer of our compaction + // in order to avoid some of the overhead of null inner-nodes + // + // thanks to the bottom layer of a binary tree containing 1/2 of all + // nodes, this brings our attr estimate down from 3t+4 -> (5/2)t+2 + + // connect previous trunk? + if (lfsr_rbyd_hastrunk(rbyd)) { + // TODO is this a bit wasteful? we're basically trashing our rcache + // read the previous tag + lfsr_tag_t tag__; + lfsr_rid_t weight__; + lfs_size_t size__; + lfs_ssize_t d = lfsr_bd_readtag(lfs, + rbyd->blocks[0], lfsr_rbyd_trunk(rbyd), + rbyd->eoff - lfsr_rbyd_trunk(rbyd), + &tag__, &weight__, &size__, NULL); + if (d < 0) { + return d; + } + + // connect with an altle + d = lfsr_bd_progtag(lfs, + rbyd->blocks[0], rbyd->eoff, + LFSR_TAG_ALT(LFSR_TAG_LE, LFSR_TAG_B, tag__), + weight__, + rbyd->eoff - lfsr_rbyd_trunk(rbyd), + &rbyd->cksum); + if (d < 0) { + return d; + } + rbyd->eoff += d; + + // reset trunk + rbyd->trunk = (rbyd->trunk & LFSR_RBYD_ISSHRUB) | 0; + + // otherwise save for next attr + } else { + rbyd->trunk = (rbyd->trunk & LFSR_RBYD_ISSHRUB) | rbyd->eoff; + } + // write the tag lfs_ssize_t d = lfsr_bd_progtag(lfs, rbyd->blocks[0], rbyd->eoff, // mark as shrub if we are a shrub - (lfsr_rbyd_isshrub(rbyd) ? LFSR_TAG_SHRUB : 0) - | tag, + (lfsr_rbyd_isshrub(rbyd) ? LFSR_TAG_SHRUB : 0) | tag, weight, lfsr_data_size(data), &rbyd->cksum); if (d < 0) { @@ -3592,6 +3648,17 @@ static int lfsr_rbyd_appendcompaction(lfs_t *lfs, lfsr_rbyd_t *rbyd, off += size__; } + // ignore single tags, unless it's the last tag, otherwise + // these should already be connected during + // lfsr_rbyd_appendcompactattr + if (!lfsr_tag_isalt(tag__) + && off-d-size__ == trunk + && off-d-size__ != lfsr_rbyd_trunk(rbyd)) { + trunk = off; + weight = 0; + continue; + } + // ignore shrub trunks, unless we are actually compacting // a shrub tree if (!lfsr_rbyd_isshrub(rbyd) @@ -3664,6 +3731,9 @@ done:; static int lfsr_rbyd_compact(lfs_t *lfs, lfsr_rbyd_t *rbyd_, lfsr_srid_t start_rid, lfsr_srid_t end_rid, const lfsr_rbyd_t *rbyd) { + // zero trunk + rbyd_->trunk = 0; + // append rbyd int err = lfsr_rbyd_appendcompactrbyd(lfs, rbyd_, start_rid, end_rid, rbyd); @@ -3722,8 +3792,8 @@ static int lfsr_rbyd_appendshrub(lfs_t *lfs, lfsr_rbyd_t *rbyd, const lfsr_shrub_t *shrub) { // keep track of the start of the new tree lfs_size_t off = rbyd->eoff; - // mark as shrub - rbyd->trunk |= LFSR_RBYD_ISSHRUB; + // mark as shrub, zero trunk + rbyd->trunk = LFSR_RBYD_ISSHRUB | 0; // compact our shrub int err = lfsr_rbyd_appendcompactrbyd(lfs, rbyd, -1, -1, shrub); @@ -4600,6 +4670,8 @@ static int lfsr_btree_commit_(lfs_t *lfs, lfsr_btree_t *btree, } // merge the siblings together + rbyd_.trunk = 0; + err = lfsr_rbyd_appendcompactrbyd(lfs, &rbyd_, -1, -1, &rbyd); if (err) { LFS_ASSERT(err != LFS_ERR_RANGE); @@ -5811,6 +5883,11 @@ static int lfsr_mdir_commit__(lfs_t *lfs, lfsr_mdir_t *mdir, // the new block && file->bshrub_.u.bsprout.u.disk.block != rbyd_.blocks[0]) { + // save our current trunk/weight + lfs_ssize_t trunk = rbyd_.trunk; + lfsr_srid_t weight = rbyd_.weight; + rbyd_.trunk = 0; + int err = lfsr_rbyd_appendcompactattr(lfs, &rbyd_, LFSR_TAG_SHRUB | LFSR_TAG_DATA, 0, file->bshrub.u.bsprout); @@ -5819,6 +5896,10 @@ static int lfsr_mdir_commit__(lfs_t *lfs, lfsr_mdir_t *mdir, return err; } + // revert rbyd trunk/weight + rbyd_.trunk = trunk; + rbyd_.weight = weight; + err = lfsr_sprout_compact(lfs, &rbyd_, &file->bshrub_.u.bsprout, &file->bshrub.u.bsprout); @@ -6098,6 +6179,9 @@ static int lfsr_mdir_compact__(lfs_t *lfs, lfsr_mdir_t *mdir_, // (btree), not the staged state (btree_), this is important, // we can't trust btree_ after a failed commit + // zero our trunk + mdir_->rbyd.trunk = 0; + // copy over tags in the rbyd in order lfsr_srid_t rid = start_rid; lfsr_tag_t tag = 0; @@ -6199,6 +6283,11 @@ static int lfsr_mdir_compact__(lfs_t *lfs, lfsr_mdir_t *mdir_, // only compact once, first compact should stage the new block && file->bshrub_.u.bsprout.u.disk.block != mdir_->rbyd.blocks[0]) { + // save our current trunk/weight + lfs_ssize_t trunk = mdir_->rbyd.trunk; + lfsr_srid_t weight = mdir_->rbyd.weight; + mdir_->rbyd.trunk = 0; + err = lfsr_rbyd_appendcompactattr(lfs, &mdir_->rbyd, LFSR_TAG_SHRUB | LFSR_TAG_DATA, 0, file->bshrub.u.bsprout); if (err) { @@ -6206,6 +6295,10 @@ static int lfsr_mdir_compact__(lfs_t *lfs, lfsr_mdir_t *mdir_, return err; } + // revert rbyd trunk/weight + mdir_->rbyd.trunk = trunk; + mdir_->rbyd.weight = weight; + err = lfsr_sprout_compact(lfs, &mdir_->rbyd, &file->bshrub_.u.bsprout, &file->bshrub.u.bsprout); if (err) { @@ -6744,6 +6837,9 @@ static int lfsr_mdir_commit(lfs_t *lfs, lfsr_mdir_t *mdir, return err; } + // zero trunk + mrootanchor_.rbyd.trunk = 0; + // copy only the config over lfsr_tag_t tag = 0; while (true) {