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:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user