Adopted a tighter, block-size dependendent attr-estimate

The concern right now is small-block filesystems, anything in the 512B
to <4KiB range. With such small blocks, and rbyd's relatively high
per-attr overhead, there's a real risk that littlefs may just not be
able to function without quickly running to metadata limits.

I realize these are pretty rare geometries for flash, but they are still
common for anything that 1. pretends to be a spinny disks, SD cards,
FTLs, eMMCs, etc, and 2. mapping into RAM, which is surprisingly common.

It is possible to require this sort of geometry to pretend to be a
larger logical block-size, but since this is a regression from the
previous version of littlefs, it would be nice to avoid this if
possible.

Anyways, what actually is this commit. Consider our tag encoding:

  .---+---+---+- -+- -+- -+- -+---+- -+- -+- -.  tag:    2 bytes
  |  tag  | weight            | size          |  weight: <=5 bytes
  '---+---+---+- -+- -+- -+- -+---+- -+- -+- -'  size:   <=4 bytes
                                                 total:  <=11 bytes

With our current 32-bit (really 31-bit) version of littlefs, the worst
case tag encoding is 11 bytes.

This doesn't sound that bad, but with our current compaction algorithm we
need ~2.5 tags for each attr:

        5t       5*11
  a_1 = -- + 2 = ---- = 30 bytes
         2         2

Are there any additional assumptions we can make to push our attr
estimate lower?

- tag - Ignoring a complete redesign of our tag encoding (which has
  already been heavily iterated over), this just needs 2 bytes, which is
  not that bad.

- weight - This is the real painful one because, for the most part,
  weight=0. But weight _can_ store a full size, in the case it is the
  root of a file's btree. So this is pretty much stuck at an annoying
  5 bytes.

  I suppose this could be tied to our size-limit. I hadn't thought about
  that until writing this commit message. Maybe that can be a future
  improvement, though it won't really have a big effect on most systems.

- size/jump - Now this field is interesting. When expressing both the
  size of tag payloads, and the relative jump offset for alt-pointers,
  this field should never exceed a single block.

  We've already pushed this down to 4 bytes at compile time, by assuming
  at most 28-bit block-sizes, but if we know the block-size, we could in
  theory push this even lower.

  This is extra enticing, because the block-sizes where the size/jump
  field can be shrunk, are _also_ the block-sizes where the metadata
  density is so critical!

So that's what this commit does. For the purpose of compaction estimates
(not stack allocations!) we calculate attr estimate based on our
runtime-determined block_size.

Here are some cutoff points for our new attr estimate:

  block-size      tag-estimate  attr-estimate
        512B  =>       9 bytes       25 bytes
       16KiB  =>      10 bytes       27 bytes
        2MiB  =>      11 bytes       30 bytes
      256MiB  =>      12 bytes       32 bytes

There is a question of when to actually do this calculation. We always
know our block-size, so we could recalculate the attr-estimate every
time we need to estimate a compaction. But for now I'm just
precalculating the attr estimate in lfs_init and storing in the lfs_t
struct. It's only a byte after all.

If I did my math correctly, we won't exceed a byte until we have a
block-size of 2^1750, at which point we may have other problems.

Code changes:

           code          stack          lfs_t
  before: 34068           2880            216
  after:  34104 (-0.1%)   2880 (+0.0%)    220 (+1.9%)

The jump in lfs_t cost is probably just from a word alignment boundary.

In the future, if we have compile-time block-sizes, the entire
attr-estimate could even be compile-time.
This commit is contained in:
Christopher Haster
2024-02-26 01:57:45 -06:00
parent d8d6052d90
commit 9fcf8e12d8
2 changed files with 42 additions and 33 deletions
+40 -33
View File
@@ -3358,30 +3358,6 @@ 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. Unfortunately,
// our inner node encoding is rather poor, requiring 2 alts and terminating
// with a 4-byte null tag:
//
// 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
// should be smaller.
@@ -3440,7 +3416,7 @@ static lfs_ssize_t lfsr_rbyd_estimate(lfs_t *lfs, const lfsr_rbyd_t *rbyd,
weight += weight_;
// include the cost of this tag
dsize_ += LFSR_ATTR_ESTIMATE + lfsr_data_size(data);
dsize_ += lfs->attr_estimate + lfsr_data_size(data);
}
if (rid == -1) {
@@ -6082,7 +6058,7 @@ static lfs_ssize_t lfsr_mdir_estimate__(lfs_t *lfs, const lfsr_mdir_t *mdir,
if (dsize__ < 0) {
return dsize__;
}
dsize_ += LFSR_ATTR_ESTIMATE + dsize__;
dsize_ += lfs->attr_estimate + dsize__;
// special handling for shrub trunks, we need to include the
// compacted cost of the shrub in our estimate
@@ -6105,11 +6081,11 @@ static lfs_ssize_t lfsr_mdir_estimate__(lfs_t *lfs, const lfsr_mdir_t *mdir,
if (dsize__ < 0) {
return dsize__;
}
dsize_ += LFSR_ATTR_ESTIMATE + dsize__;
dsize_ += lfs->attr_estimate + dsize__;
} else {
// include the cost of this tag
dsize_ += LFSR_ATTR_ESTIMATE + lfsr_data_size(data);
dsize_ += lfs->attr_estimate + lfsr_data_size(data);
}
}
@@ -9988,7 +9964,7 @@ static int lfsr_bshrub_commit(lfs_t *lfs, lfsr_file_t *file, lfsr_bid_t bid,
// only include tag overhead if tag is not a grow/rm tag
if (!lfsr_tag_isgrow(attrs[i].tag)
&& !lfsr_tag_isrm(attrs[i].tag)) {
commit_estimate += LFSR_ATTR_ESTIMATE;
commit_estimate += lfs->attr_estimate;
}
commit_estimate += lfsr_data_size(attrs[i].data);
}
@@ -15137,7 +15113,38 @@ static int lfs_init(lfs_t *lfs, const struct lfs_config *cfg) {
lfs->hasorphans = false;
// compute the number of bits we need to reserve for mdir rids
// calculate 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. Unfortunately,
// our inner node encoding is rather poor, requiring 2 alts and terminating
// with a 4-byte null tag:
//
// 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
//
// The worst-case tag encoding, t, actually depends on our block_size,
// since the size/jump field can never exceed a block:
//
// t = 2 + 5 + log128(block_size)
//
uint8_t tag_estimate = 2 + 5 + (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;
// calculate the number of bits we need to reserve for mdir rids
//
// Worst case (or best case?) each metadata entry is a single tag. In
// theory each entry also needs a name, but with power-of-two rounding,
@@ -15146,9 +15153,9 @@ static int lfs_init(lfs_t *lfs, const struct lfs_config *cfg) {
// Assuming a _perfect_ compaction algorithm (requires unbounded RAM),
// each tag also needs ~1 alt, this gives us:
//
// block_size
// m = ----------
// 2t
// block_size block_size
// m = ---------- = ----------
// a_inf 2t
//
// Assuming t=4 bytes, the minimum tag encoding:
//
+2
View File
@@ -598,7 +598,9 @@ typedef struct lfs {
// purpose flags field? this has been useful for lfsr_file_t
bool hasorphans;
uint8_t attr_estimate;
uint8_t mleaf_bits;
lfsr_mdir_t mroot;
lfsr_mtree_t mtree;