Opportunistically stitch together range removals with red alts

This is intending to improve some balancing issues with range removals
in our rbyds.

Consider the following range removal, this is our current algorithm:

                                          .-----------o
         .-------o-------.                |           o-----------.
     .---o---.       .---o---.            |     .-----o           |
   .-o-.   .-o-.   .-o-.   .-o-.        .-o-.   |     o-----.   .-o-.
  .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  =>  a b c d e f g     j k l m n o p
               '-+-'
               remove

Somehow the height of the tree increased! Even though we are only
removing nodes. Not great.

The reason this happens is because we are trying to stitch together the
two search paths that occur when our range diverges. Naively, with
binary nodes, this results in a worst case of ~2x the diverged height.

If only there was a way to represent a ternary node... Wait, isn't this
what our red alts are for?

Recall that in a red-black(-yellow) tree, red edges are a coloring that
represent a 2-3-4 node with 3 branches. If, as we stitch together our
two search paths, we alternate between red and black alts, we can avoid
a height increase in the underlying 2-3-4 tree!

         .-------o-------.
     .---o---.       .---o---.            .-----------r-----------.
   .-o-.   .-o-.   .-o-.   .-o-.        .-o-.   .-----r-----.   .-o-.
  .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  =>  a b c d e f g     j k l m n o p
               '-+-'
               remove

This works great if all our nodes are black. Unfortunately, if we
already have red alts, this doesn't always work. We can't connect red
alts with red alts, or we risk breaking invariants:

                                    .---+-------r
       .-------o-------.            |   |       r-------+---.
   .---r----.     .----r---.        |   |  .-+--r       |   |
  .o. .o. .-r-. .-r-. .o. .o.      .o. .o. | |  o--+-. .o. .o.
  a b c d e f g h i j k l m n  =>  a b c d e f     i j k l m n
             '-+-'
             remove

If we ignore red alts, and pretend they are black during range removals,
we just end up with a slightly permuted tree:

                                    .-----------r-----------.
       .-------o-------.            |   .-------r-------.   |
   .---r----.     .----r---.        |   |  .----r----.  |   |
  .o. .o. .-r-. .-r-. .o. .o.      .o. .o. | .--o--. | .o. .o.
  a b c d e f g h i j k l m n  =>  a b c d e f     i j k l m n
             '-+-'
             remove

It's tempting to try to stitch red nodes together with yellow alts, but
this breaks the invariant that the our parent is never yellow during
append, forcing append to be potentially recursive if we encounter
a naturally occuring yellow alt.

With a hypothetical 5-branch node however...

But at least this delays unbalancing when black alts are present.

And, since we downgrade any red alts when pruning during range removals,
we should end up with more black alts available for opportunistic
stitching than in the original tree.

---

Surprisingly the code cost ended up breaking even, probably because of
some minor code cleanup in the function:

  before: 34072          2880
  after:  34072 (+0.0%)  2880 (+0.0%)

Measuring performance with a quick file random-write benchmark showed
a noticable but tiny improvement. Though it may be 1. too close of to
the noise floor to be trustworthy, 2. not really rigorous, and 3. file
random-write may not hit dgenerate range removals. But hey, at least it
doesn't show a negative impact on performance.
This commit is contained in:
Christopher Haster
2024-03-03 14:00:15 -06:00
parent 34be5055b4
commit bedb65919c
+16 -8
View File
@@ -2808,13 +2808,14 @@ static int lfsr_rbyd_appendattr(lfs_t *lfs, lfsr_rbyd_t *rbyd,
p_alts[0], p_weights[0],
lower_rid, upper_rid,
other_rid_, other_tag_)) {
// first take care of any lingering red alts
// take care of any lingering red alts before diverging
if (lfsr_tag_isred(p_alts[0])) {
alt = p_alts[0] & ~LFSR_TAG_R;
weight = p_weights[0];
jump = p_jumps[0];
branch_ = branch;
lfsr_rbyd_p_pop(p_alts, p_weights, p_jumps);
// begin diverging
} else {
tag_ |= LFSR_TAG_DIVERGED | LFSR_TAG_DIVERGEDLOWER;
other_tag_ |= LFSR_TAG_DIVERGED | LFSR_TAG_DIVERGEDUPPER;
@@ -2996,17 +2997,22 @@ static int lfsr_rbyd_appendattr(lfs_t *lfs, lfsr_rbyd_t *rbyd,
lfs_swap32(&jump, &branch_);
}
// trim alt from our current bounds
if (lfsr_tag_isblack(alt)) {
// trim alt from our current bounds
lfsr_tag_trim2(
alt, weight,
p_alts[0], p_weights[0],
&lower_rid, &upper_rid,
&lower_tag, &upper_tag);
// if diverged, stitch our paths together with alternating
// red alts
if (lfsr_tag_hasdiverged(tag_)
&& p_alts[0]
&& !lfsr_tag_isred(p_alts[1])) {
p_alts[0] |= LFSR_TAG_R;
}
}
// continue to next alt
graft = branch;
branch = branch_;
// push alt onto our queue
int err = lfsr_rbyd_p_push(lfs, rbyd,
@@ -3016,6 +3022,10 @@ static int lfsr_rbyd_appendattr(lfs_t *lfs, lfsr_rbyd_t *rbyd,
return err;
}
// continue to next alt
graft = branch;
branch = branch_;
// found end of tree?
} else {
// update the found tag/rid
@@ -3032,9 +3042,7 @@ static int lfsr_rbyd_appendattr(lfs_t *lfs, lfsr_rbyd_t *rbyd,
}
// switch to the other path if we have diverged
if (!lfsr_tag_isalt(alt)
|| (lfsr_tag_hasdiverged(tag_)
&& lfsr_tag_isblack(p_alts[0]))) {
if (lfsr_tag_hasdiverged(tag_) && lfsr_tag_isblack(p_alts[0])) {
lfs_swap16(&tag_, &other_tag_);
lfs_sswap32(&rid_, &other_rid_);
lfs_swap32(&branch, &other_branch);