From 120f0a2e17df2768d910214ac8ea917dd73fcbe4 Mon Sep 17 00:00:00 2001 From: Christopher Haster Date: Fri, 29 Mar 2024 15:11:11 -0500 Subject: [PATCH] rbyd-rr: Cleanup of new structure-preserving diverging algorithm Since this set of changes are fairly stable now, and show improved balancing during range operations, it's probably a good checkpoint to summarize the changes to the diverging range-removal algorithm. From a high-level, the range-removal algorithm is mostly unchanged: 1. Guess if we are performing a range operation. This is determined by the delta and sup/sub bits. If we aren't, do a normal append. 2. Diverging-lower: Start traversing the rbyd, but don't write out any alts yet. If we find an alt where our range would diverge, transition to the next step. If we don't, fall back to a normal append. This requested range contains no alts in this case. 3. Diverged-lower: Write out alts < requested range. Keep track of the resulting lower trunk and lower bound. 4. Diverging-upper: Reset and start traversing the rbyd again, this time writing out all alts that we know are common. This will become our actual trunk. When we find the diverging alt this time, replace it with a stitching alt that points to the lower trunk. 5. Diverged-upper: Write out alts > requested range. 6. Create a new leaf alt as normal, but using the lower trunk's lower bound and upper trunk's upper bound. What has changed is how we prune alts in the requested range after we've found the diverging alt. Previously, we would simply remove these alts from the tree, but this would throw away color information and result in an unbalanced 2-3-4 tree. Not an immediately obvious issue since the actual binary tree stays more-or-less balanced, but as more rbyd operations pile on the self-balancing breaks, and the resulting tree could become up to ~2x unabalanced: .-------o-------. .---o---. .---o---. .-o-. .-o-. .-o-. .-o-. .o. .o. .o. .o. .o. .o. .o. .o. a b c d e f g h i j k l m n o p '------+------' remove .--------o .---o---. | .-o-. .-o-. | .o. .o. .o. .o. | a b c d e f g h i ^ append j'k'l' .-----o .--------o .-+-r .---o---. | | | | .-o-. .-o-. | | | | .o. .o. .o. .o. | | | | a b c d e f g h i j'k'l' ^ append m'n'o'p'q'r' .-------------o .---o .---+-----r .--------o .-o .-o .-o .-+-r .---o---. | | | | | | | | | | .-o-. .-o-. | | | | | | | | | | .o. .o. .o. .o. | | | | | | | | | | a b c d e f g h i j'k'l'm'n'o'p'q'r' Now, instead, we preserve 2-3-4 nodes by only removing alts that are red or have a red neighbor. Black alts are not removed, but instead converted to "alt-never" (altn) alts that represent a sort of empty 1-node: .---> a rm me | .-> b red prune .-> b -r-b-> c => ---b-> c .-> a rm me v-------- altn .-b-> b black flatten .-b-> b -. | .-> c => | .-> c +- note the tree is balanced -b-b-> d -b-b-> d -' lfsr_rbyd_p_recolor is extended such that if we push up a red alt into an altn, instead of recoloring red, we just reclaim the altn. This effectively transitions from a 1-node -> 2-node in the same way recoloring transitions from a 2-node -> 3-node or 3->node -> 4-node: .-> a' .-> a' .-b-> b insert a' .-r-b-> b reclaim altn .-b-> b | .-> c => | .-> c => | .-> c -b-b-> d -b-b-> d -b-b-> d The result, counterintuitively, is that by introducing otherwise unecessary altns, we can preserve the structure of the 2-3-4 tree and better preserve the balance of the tree: .-------o-------. .---o---. .---o---. .-o-. .-o-. .-o-. .-o-. .o. .o. .o. .o. .o. .o. .o. .o. a b c d e f g h i j k l m n o p '------+------' remove .--------o .---o---. o .-o-. .-o-. o .o. .o. .o. .o. o a b c d e f g h i ^ append j'k'l'm' .----------------o .---o---. o .-o-. .-o-. .------o .o. .o. .o. .o. .o. .-+-r a b c d e f g h i j'k'l'm' ^ append n'o'p'q'r's' .----------------------------o .---o---. .-------------o .-o-. .-o-. .---o .---+-----r .o. .o. .o. .o. .-o .-o .-o .-o .-+-r a b c d e f g h i j'k'l'm'n'o'p'q'r's' Though I guess altns technically make this a 1-2-3-4 tree... Note that this algorithm does _not_ maintain a strictly balanced tree in terms of the current number of attrs, h<=log n. But it _does_ maintain a balanced tree in terms of the worst possible sequence of append operations. And since our rbyd are bounded by our block size, this is strictly h<=log b. --- This algorithm, as implemented, is not perfect. We are correctly maintaining the 2-3-4 structure both before and after the tree diverges, but this is a bit hand-wavey about the diverging alt itself. And the diverging alt proves to be annoyingly tricky. We want to replace the diverging alt with a stitching alt to tie together the lower and upper diverged paths, but doing so while maintaining the color the diverging alt interacts with later red flips and yellow splits in _very_ ugly ways. The solution right now is to just unconditionally recolor the diverging alt black. This avoids a whole set of diverged-recoloring issues, but does risk unbalancing our tree by +1 if we diverge on a red alt. Still, this is a significant improvement over the +~2x of the previous algorithm. And the altns introduce significant flexiblity into the tree, so it may be possible to avoid this +1 unbalancing at some point in the future. --- This commit is mainly a cleanup commit, removing commented-out code, debugging printfs, asserts, etc. Other minor changes: - Move y_branch updates to beginning of alt loop, instead of in every single branch tail. - Deduplicated black recoloring in lfsr_rbyd_p_recolor again. - Made leaf-split red recoloring unconditional, since all leaf-split alts are now red. This is a good sign that our new algorithm is more correct. Now that the dust has settled, we can look into how these algorithm tweaks impact code cost: code stack rr-div-naive: 33968 2864 rr-div-altn: 34308 (+1.0%) 2864 (+0.0%) If we focus on lfsr_rbyd_appendattr, which contains almost all of the actual diverging logic, we can also compare against the original naive stitching algorithm (rr-stitching). Keep in mind rr-stitching could increase the binary height by ~2x, naive diverging (rr-div-naive) the 2-3-4 height by ~2x, and our current algorithm (rr-div-altn) the 2-3-4 height by ~1: code frame stack appendattr rr-stitching: 1940 184 536 appendattr rr-div-naive: 2028 (+4.5%) 200 (+8.7%) 552 (+3.0%) appendattr rr-div-altn: 2584 (+33.2%) 232 (+26.1%) 584 (+9.0%) Unfortunately our new algorithm does end up costly. This seems to mainly be due to the extra altn-specific logic, as well as the more complicated pruning logic. Maybe the pruning logic deserves more work? Still, the value is having an actually correct algorithm. And thanks to altns, we have much stronger proofs over how range operations affect the underlying 2-3-4 tree balance. --- lfs.c | 129 ++++++++++++---------------------------------------------- 1 file changed, 26 insertions(+), 103 deletions(-) diff --git a/lfs.c b/lfs.c index bc96fc3c..79f786af 100644 --- a/lfs.c +++ b/lfs.c @@ -2693,6 +2693,8 @@ static void lfsr_rbyd_p_recolor( lfsr_rid_t p_weights[static 3], lfs_size_t p_jumps[static 3]) { // propagate a red edge upwards + p_alts[0] &= ~LFSR_TAG_R; + if (p_alts[1]) { p_alts[1] |= LFSR_TAG_R; @@ -2776,8 +2778,6 @@ static int lfsr_rbyd_appendattr(lfs_t *lfs, lfsr_rbyd_t *rbyd, return 0; } - printf("%04x->%04x: appendattr:\n", lfsr_rbyd_trunk(rbyd), rbyd->eoff); - // begin appending int err = lfsr_rbyd_prepareappend(lfs, rbyd); if (err) { @@ -2888,6 +2888,11 @@ again:; // descend down tree, building alt pointers while (true) { + // keep track of incoming branch + if (lfsr_tag_isblack(p_alts[0])) { + y_branch = branch; + } + // read the alt pointer lfsr_tag_t alt; lfsr_rid_t weight; @@ -2898,13 +2903,6 @@ again:; if (d < 0) { return d; } - printf("%04x->%04x: tag 0x%x w%d (%d %d)\n", - branch, - rbyd->eoff, - alt, - weight, - lower_rid, - upper_rid); // found an alt? if (lfsr_tag_isalt(alt)) { @@ -2927,11 +2925,7 @@ again:; // note that if red alt diverged if would have been caught // on the previous pass d_state = lfsr_d_diverge(d_state); - printf("%04x->%04x: diverging 0x%x w%d\n", - branch, - rbyd->eoff, - alt, - weight); + if (lfsr_tag_follow2( alt, weight, p_alts[0], p_weights[0], @@ -2951,12 +2945,6 @@ again:; // stitch together diverged branches if (d_state == LFSR_D_DIVERGEDUPPER) { - printf("%04x->%04x: stitching: 0x%x w%d, 0x%x\n", - branch, - rbyd->eoff, - d_tag, - d_rid - lower_rid, - d_branch); err = lfsr_rbyd_p_push(lfs, rbyd, p_alts, p_weights, p_jumps, LFSR_TAG_ALT(LFSR_TAG_LE, LFSR_TAG_B, d_tag), @@ -2965,11 +2953,8 @@ again:; if (err) { return err; } - - //lfsr_rbyd_p_recolor(p_alts, p_weights, p_jumps); } - y_branch = branch; branch = branch_; continue; @@ -2989,24 +2974,12 @@ again:; &lower_rid, &upper_rid, &lower_tag, &upper_tag); - // TODO can we move y_branch updates to beginning of loop? - y_branch = branch; branch = branch_; continue; - } -// if (lfsr_d_isdiverged(d_state)) { -// alt &= ~LFSR_TAG_R; -// } - -// // TODO better solution? -// // always prune alt-always tags -// if (lfsr_tag_isa(alt)) { -// goto prune; -// } // trim unreachable alts created by diverged paths so they // will be pruned - if (lfsr_d_isdiverged(d_state) + } else if (lfsr_d_isdiverged(d_state) && (d_state == LFSR_D_DIVERGEDUPPER) ^ lfsr_tag_isgt(alt) ^ lfsr_tag_follow2( @@ -3046,9 +3019,9 @@ again:; p_alts[0], p_weights[0], lower_rid, upper_rid, lower_tag, upper_tag)) { - // note, yellow prunes always follow and have no weight, it's + // note, yellow pruning always follows and has no weight, it's // only diverged pruning that needs all these special cases - + // // eagerly flip in case we are ambiguous yellow alts if (lfsr_tag_follow2( alt, weight, @@ -3060,11 +3033,6 @@ again:; // collapse unreachable red alts if (lfsr_tag_isred(p_alts[0])) { - printf("%04x->%04x: rprune 0x%x w%d\n", - branch, - rbyd->eoff, - alt, - weight); alt = p_alts[0] & ~LFSR_TAG_R; weight = p_weights[0]; jump = p_jumps[0]; @@ -3074,26 +3042,15 @@ again:; // make unreachable black alts alt-nevers, if we prune these // it would break the coloring of our tree } else { - printf("%04x->%04x: bprune 0x%x w%d\n", - branch, - rbyd->eoff, - alt, - weight); alt = LFSR_TAG_ALT(LFSR_TAG_LE, LFSR_TAG_B, 0); weight = 0; -// jump = 0; + jump = 0; } } // two reds makes a yellow, split? if (lfsr_tag_isred(alt) && lfsr_tag_isred(p_alts[0])) { LFS_ASSERT(lfsr_tag_isparallel(alt, p_alts[0])); - printf("%04x->%04x: ysplit 0x%x w%d 0x%x\n", - branch, - rbyd->eoff, - alt, - weight, - y_branch); // if we take the red or yellow alt we can just point // to the black alt @@ -3123,7 +3080,6 @@ again:; p_alts[0], p_weights[0], &lower_rid, &upper_rid, &lower_tag, &upper_tag); - p_alts[0] &= ~LFSR_TAG_R; lfsr_rbyd_p_recolor(p_alts, p_weights, p_jumps); // otherwise we need to point to the yellow alt and @@ -3147,19 +3103,8 @@ again:; p_alts[0], p_weights[0], &lower_rid, &upper_rid, &lower_tag, &upper_tag); - p_alts[0] &= ~LFSR_TAG_R; lfsr_rbyd_p_recolor(p_alts, p_weights, p_jumps); -// // keep track of last alt on diverged trunk to stitch the -// // trunks together with -// if ((d_state == LFSR_D_DIVERGEDLOWER -// || (d_state == LFSR_D_NOTDIVERGING -// && lfsr_tag_isle(p_alts[0]))) -// && !lfsr_tag_isn(p_alts[0])) { -// d_tag = p_alts[0]; -// // d_rid = lower_rid; -// } - branch = branch_; continue; } @@ -3211,16 +3156,6 @@ again:; p_alts[0], p_weights[0], &lower_rid, &upper_rid, &lower_tag, &upper_tag); - -// // keep track of last alt on diverged trunk to stitch the -// // trunks together with -// if ((d_state == LFSR_D_DIVERGEDLOWER -// || (d_state == LFSR_D_NOTDIVERGING -// && lfsr_tag_isle(alt))) -// && !lfsr_tag_isn(alt)) { -// d_tag = alt; -// // d_rid = lower_rid; -// } } // push alt onto our queue @@ -3232,7 +3167,6 @@ again:; } // continue to next alt - y_branch = branch; branch = branch_; continue; @@ -3250,17 +3184,11 @@ again:; // no divergence? guess we only need one trunk then, actually write // it out this time if (d_state == LFSR_D_DIVERGINGLOWER) { - printf("%04x->%04x: not diverging\n", - branch, - rbyd->eoff); d_state = LFSR_D_NOTDIVERGING; goto again; // diverged lower trunk? we need an upper trunk too } else if (d_state == LFSR_D_DIVERGEDLOWER) { - printf("%04x->%04x: diverging switch\n", - branch, - rbyd->eoff); // keep track of last alt on diverged trunk to stitch the trunks // together with d_state = LFSR_D_DIVERGINGUPPER; @@ -3275,16 +3203,14 @@ again:; } // terminate diverged trunk with an unreachable tag -// if (d_tag) { - err = lfsr_rbyd_appendattr_(lfs, rbyd, - (lfsr_rbyd_isshrub(rbyd) ? LFSR_TAG_SHRUB : 0) - | LFSR_TAG_NULL, - 0, - LFSR_DATA_NULL()); - if (err) { - return err; - } -// } + err = lfsr_rbyd_appendattr_(lfs, rbyd, + (lfsr_rbyd_isshrub(rbyd) ? LFSR_TAG_SHRUB : 0) + | LFSR_TAG_NULL, + 0, + LFSR_DATA_NULL()); + if (err) { + return err; + } // swap tag/rid and write out the upper trunk lfs_swap16(&a_tag, &b_tag); @@ -3326,12 +3252,12 @@ again:; && lfsr_tag_key(tag_) < lfsr_tag_key(tag)))))) { if (lfsr_tag_isrm(tag) || !lfsr_tag_key(tag)) { // if removed, make our tag unreachable - alt = LFSR_TAG_ALT(LFSR_TAG_GT, LFSR_TAG_R, lower_tag); + alt = LFSR_TAG_ALT(LFSR_TAG_GT, LFSR_TAG_B, lower_tag); weight = upper_rid - lower_rid + delta; upper_rid -= weight; } else { // split less than - alt = LFSR_TAG_ALT(LFSR_TAG_LE, LFSR_TAG_R, tag_); + alt = LFSR_TAG_ALT(LFSR_TAG_LE, LFSR_TAG_B, tag_); weight = upper_rid - lower_rid; lower_rid += weight; } @@ -3347,12 +3273,12 @@ again:; && lfsr_tag_key(tag_) > lfsr_tag_key(tag)))))) { if (lfsr_tag_isrm(tag) || !lfsr_tag_key(tag)) { // if removed, make our tag unreachable - alt = LFSR_TAG_ALT(LFSR_TAG_GT, LFSR_TAG_R, lower_tag); + alt = LFSR_TAG_ALT(LFSR_TAG_GT, LFSR_TAG_B, lower_tag); weight = upper_rid - lower_rid + delta; upper_rid -= weight; } else { // split greater than - alt = LFSR_TAG_ALT(LFSR_TAG_GT, LFSR_TAG_R, tag); + alt = LFSR_TAG_ALT(LFSR_TAG_GT, LFSR_TAG_B, tag); weight = upper_rid - (rid+1); upper_rid -= weight; } @@ -3366,11 +3292,8 @@ again:; return err; } - if (lfsr_tag_isred(p_alts[0])) { - // introduce a red edge - p_alts[0] &= ~LFSR_TAG_R; - lfsr_rbyd_p_recolor(p_alts, p_weights, p_jumps); - } + // introduce a red edge + lfsr_rbyd_p_recolor(p_alts, p_weights, p_jumps); } // flush any pending alts