Reverted the improved (5/2)t+2 rbyd compaction algorithm

Now back to our naive compaction algorithm:

  a_0 = 3t + 4

Why? The main reason is simplicity:

- In order to connect the first layer, we need to maintain some state
  external to lfsr_rbyd_appendcompactattr, mainly rbyd.trunk which needs
  to be zeroed before compaction.

  This is a great recipe for bugs.

- lfsr_rbyd_appendcompaction needs a couple hacks to avoid redundantly
  connecting the first layer, while also not missing any lingering
  attrs/trunks in non-power-of-two trees. Though these hacks at least
  fall into the stupid + works = not stupid category.

- Our interaction with the rcache is bad.

  Before, our compaction algorithm sort of operated in two modes:

  1. Append attrs - Without any rbyd structure, we only need the pcache
     to append, allowing the rcache to be used entirely to, you know,
     cache whatever is sourcing our attrs.

  2. Build the balanced rbyd tree - At this point we need the rcache to
     read rbyd trunks to connect everything, but we are also probably
     done with the attr source.

  But our improved compaction algorithm muddies step 1., now we need
  the rcache to connect layer 1, which may thrash our attr source.

  To be clear, this only happens because we reread the previous trunk
  for the tag and weight. If we cached everything needed to build the
  alt-pointer in RAM, we could avoid this, but we currently don't have
  enough space in our rbyd struct.

So I'm reverting for now. Though I've left a comment noting some
theoreticals.

I think a better design would be to change lfsr_rbyd_appendcompactattr
to take an optional array of trunks (along with tags+weights), which
could be user configurable to trade RAM cost for metadata density.

It would be extra interesting to see such a configurable array tuned to
the current maximum stack depth, in theory making it effectively free.

But I think this is out-of-scope for now.

Now that tag-estimate has gotten complicated, here's a quick matrix to
note how attr-estimate changes with this revert (a_0):

  block-size      a_0    a_1  a_inf
        512B  =>   31     25     18  (bytes)
       16KiB  =>   34     27     20  (bytes)
        2MiB  =>   37     30     22  (bytes)
      256MiB  =>   40     32     24  (bytes)

Code changes:

           code          stack
  before: 34132           2880
  after:  33912 (-0.6%)   2880 (+0.0%)
This commit is contained in:
Christopher Haster
2024-02-26 23:23:04 -06:00
parent 21d9535a05
commit 907d6b7038
+15 -94
View File
@@ -3462,47 +3462,6 @@ 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
@@ -3624,17 +3583,6 @@ 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)
@@ -3707,9 +3655,6 @@ 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);
@@ -3768,8 +3713,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, zero trunk
rbyd->trunk = LFSR_RBYD_ISSHRUB | 0;
// mark as shrub
rbyd->trunk |= LFSR_RBYD_ISSHRUB;
// compact our shrub
int err = lfsr_rbyd_appendcompactrbyd(lfs, rbyd, -1, -1, shrub);
@@ -4646,8 +4591,6 @@ 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);
@@ -5859,11 +5802,6 @@ 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);
@@ -5872,10 +5810,6 @@ 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);
@@ -6155,9 +6089,6 @@ 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;
@@ -6259,11 +6190,6 @@ 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) {
@@ -6271,10 +6197,6 @@ 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) {
@@ -6813,9 +6735,6 @@ 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) {
@@ -8655,15 +8574,15 @@ int lfsr_mkdir(lfs_t *lfs, const char *path) {
//
// Worst case (or best case?) each directory needs 1 name tag, 1 did
// tag, and 1 bookmark. With our current compaction strategy, each tag
// needs ~(5/2)t+2 bytes for tag+alts (see our attr_estimate). And, if
// needs 3t+4 bytes for tag+alts (see our attr_estimate). And, if
// we assume ~1/2 block utilization due to our mdir split threshold, we
// can multiply everything by 2:
//
// d = 3 * ((5/2)t+2) * 2 = 15t + 12
// d = 3 * (3t+4) * 2 = 18t + 24
//
// Assuming t=4 bytes, the minimum tag encoding:
//
// d = 15*4 + 12 = 72 bytes
// d = 18*4 + 24 = 96 bytes
//
// Rounding down to a power-of-two (again this is all arbitrary), gives
// us ~64 bytes per directory:
@@ -15147,13 +15066,15 @@ static int lfs_init(lfs_t *lfs, const struct lfs_config *cfg) {
//
// 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:
// Or, if you build a bounded number of layers perfectly:
//
// 3t + 4 2t 5t
// a_1 = ------ + -- = -- + 2
// 2 2 2
// 2t 3t + 4
// a_1 = -- + ------
// 2 2
//
// a_n = 2t*(1-2^-n) + (3t + 4)*2^-n
//
// But this would be a tradeoff in code complexity.
//
// The worst-case tag encoding, t, depends on our size-limit and
// block-size. The weight can never exceed size-limit, and the size/jump
@@ -15169,7 +15090,7 @@ static int lfs_init(lfs_t *lfs, const struct lfs_config *cfg) {
+ (lfs_nlog2(lfs->size_limit+1)+7-1)/7
+ (lfs_nlog2(lfs->cfg->block_size)+7-1)/7;
LFS_ASSERT(tag_estimate <= LFSR_TAG_DSIZE);
lfs->attr_estimate = (5*tag_estimate+2-1)/2 + 2;
lfs->attr_estimate = 3*tag_estimate + 4;
// calculate the number of bits we need to reserve for mdir rids
//
@@ -15194,7 +15115,7 @@ static int lfs_init(lfs_t *lfs, const struct lfs_config *cfg) {
// temporarily fill with more mids before compaction occurs.
//
// Note note our actual compaction algorithm is not perfect, and
// requires (5/2)t+2 bytes per tag, or with t=4 bytes => ~block_size/12
// requires 3t+4 bytes per tag, or with t=4 bytes => ~block_size/12
// metadata entries per block. But we intentionally don't leverage this
// to maintain compatibility with a theoretical perfect implementation.
//