Improved rbyd compaction algorithm, reduced attr estimate 3t+4 -> (5/2)t+2
The motivation for this is that the rbyd inner node encoding during
compaction is kind-of not that great.
Our alt encoding is great when the trunk terminates in a tag, which is
how it was originally designed to be used:
00000004: data w1 1 61 a <.
00000020: altble 0x300 w1 0x4 -' <-- trunk
00000024: data w1 1 62 b
But when used to create an arbitrary binary-tree inner node, the best
encoding I can think of is 2 alts + a terminating null tag, which is not
that great:
00000004: data w1 1 61 a <.
00000009: data w1 1 62 b <--.
0000000e: altble 0x300 w1 0x4 -' | <-- trunk
00000012: altble 0x300 w1 0x9 ---'
00000016: null
This effects our attr estimate, which is defined as the worst-case
on-disk cost of an attr after compaction. This is an important value,
as it determines when we split rbyds. And it effectively determines how
densely we can store metadata without needing to worry about block
overflow issues.
In our current compaction algorithm, we connect each attr with a 2 alt +
null inner node. Since we are creating a perfectly balanced binary tree,
this works out to ~1 inner node per attr. Including the attr's data tag,
this gives us:
a_0 = 3t + 4
Where t is the tag estimate, currently a 2 byte tag, <=5 byte weight,
<=4 byte size, t = 2+5+4 = 11 bytes:
a_0 = 3*11 + 4 = 37 bytes
Though this may be vary across different littlefs configurations,
16-bit, 64-bit, etc.
---
It would be great if our compaction algorithm could build each trunk
perfectly, as each attr is written. In such a case, each attr
theoretically only needs ~1 alt and the attr's data tag:
a_inf = 2t
Or, assuming t = 11 bytes:
a_inf = 2*11 = 22 bytes
Unfortunately, as far as I can tell, this fundamentally requires
unbounded RAM. You need to keep track of log n previous trunks in order
to always build the next trunk perfectly, and log n is > 1.
I suppose in theory you could implement a O(n^2) algorithm that
repeatedly scans for the previous trunks... But that would be a
hilarious regression since the whole point of this work was to reduce
compaction from O(n^2) -> O(log n).
---
However, we can meet halfway. Consider what happens if we build perfect
trunks for only the bottom layer of the rbyd.
This may not seem like it will gain much, but remember that in a binary
tree, the bottom layer contains ~1/2 of the total nodes in the tree:
3t + 4 2t 5t
a_1 = ------ + -- = -- + 2
2 2 2
Or, assuming t = 11 bytes:
5*11
a_1 = ---- + 2 = 30 bytes
2
Not too shabby for a constant amount of RAM.
In theory this could be extended to n layers, by keeping a
(configurable?) array of previous trunks in RAM during compaction, but
this would have diminishing results as we move up the tree.
With only needing to keep track of one other trunk, we can even store
this in rybd.trunk, which is currently unused during compaction. So zero
extra RAM.
The resulting compaction looks like the following:
00000004: data w1 1 61 a <.
00000020: altble 0x300 w1 0x4 <--.
00000024: data w1 1 62 b |
00000029: data w1 1 63 c <. |
0000002e: altble 0x300 w1 0x29 <. |
00000032: data w1 1 64 d | |
00000037: altble 0x300 w2 0x20 -' | <-- trunk
0000003b: altble 0x300 w2 0x2e ---'
0000003f: null
This does make compaction a bit more complicated, which is reflected in
the code size:
code stack
before: 33856 2880
after: 34068 (+0.6%) 2880 (+0.0%)
Some implementation things to note:
- Since lfsr_rbyd_appendcompactattr now has state, we need to make sure
to zero the trunk before compacting, which adds an annoying bit of
bookkeeping everywhere.
- I didn't want to add extra state to manage in
lfsr_rbyd_appendcompactattr calls, so this implementation only tracks
the previous trunk offset, and needs a readtag call to get the
tag+weight necessary to build the actual alt pointer.
This may have some unpleasant interactions with the rcache, and may be
worth revisiting.
This commit is contained in:
@@ -3360,11 +3360,27 @@ static int lfsr_rbyd_commit(lfs_t *lfs,
|
||||
|
||||
// determine the upper-bound cost of a single rbyd attr after compaction
|
||||
//
|
||||
// note that with rebalancing during compaction, we know the number
|
||||
// of inner nodes is roughly the same as the number of tags. Each node
|
||||
// has two alts and is terminated by a 4-byte null tag.
|
||||
// Note that with rebalancing during compaction, we know the number
|
||||
// of inner nodes is roughly the same as the number of tags. Unfortunately,
|
||||
// our inner node encoding is rather poor, requiring 2 alts and terminating
|
||||
// with a 4-byte null tag:
|
||||
//
|
||||
#define LFSR_ATTR_ESTIMATE (3*LFSR_TAG_DSIZE + 4)
|
||||
// a_0 = 3t + 4
|
||||
//
|
||||
// If we could build each trunk perfectly, we could get this down to only
|
||||
// 1 alt per tag. But this would require unbounded RAM:
|
||||
//
|
||||
// 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:
|
||||
//
|
||||
// 3t + 4 2t 5t
|
||||
// a_1 = ------ + -- = -- + 2
|
||||
// 2 2 2
|
||||
//
|
||||
#define LFSR_ATTR_ESTIMATE ((5*LFSR_TAG_DSIZE+2-1)/2 + 2)
|
||||
|
||||
// Calculate the maximum possible disk usage required by this rbyd after
|
||||
// compaction. This uses a conservative estimate so the actual on-disk cost
|
||||
@@ -3470,11 +3486,51 @@ 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
|
||||
(lfsr_rbyd_isshrub(rbyd) ? LFSR_TAG_SHRUB : 0)
|
||||
| tag,
|
||||
(lfsr_rbyd_isshrub(rbyd) ? LFSR_TAG_SHRUB : 0) | tag,
|
||||
weight, lfsr_data_size(data),
|
||||
&rbyd->cksum);
|
||||
if (d < 0) {
|
||||
@@ -3592,6 +3648,17 @@ 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)
|
||||
@@ -3664,6 +3731,9 @@ 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);
|
||||
@@ -3722,8 +3792,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
|
||||
rbyd->trunk |= LFSR_RBYD_ISSHRUB;
|
||||
// mark as shrub, zero trunk
|
||||
rbyd->trunk = LFSR_RBYD_ISSHRUB | 0;
|
||||
|
||||
// compact our shrub
|
||||
int err = lfsr_rbyd_appendcompactrbyd(lfs, rbyd, -1, -1, shrub);
|
||||
@@ -4600,6 +4670,8 @@ 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);
|
||||
@@ -5811,6 +5883,11 @@ 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);
|
||||
@@ -5819,6 +5896,10 @@ 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);
|
||||
@@ -6098,6 +6179,9 @@ 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;
|
||||
@@ -6199,6 +6283,11 @@ 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) {
|
||||
@@ -6206,6 +6295,10 @@ 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) {
|
||||
@@ -6744,6 +6837,9 @@ 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) {
|
||||
|
||||
Reference in New Issue
Block a user