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.
This commit is contained in:
Christopher Haster
2024-03-29 15:11:11 -05:00
parent 7375172148
commit 120f0a2e17
+26 -103
View File
@@ -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