From 9fcf8e12d8d270da1148eb38418a3d42bd816ba4 Mon Sep 17 00:00:00 2001 From: Christopher Haster Date: Mon, 26 Feb 2024 01:57:45 -0600 Subject: [PATCH] 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. --- lfs.c | 73 ++++++++++++++++++++++++++++++++--------------------------- lfs.h | 2 ++ 2 files changed, 42 insertions(+), 33 deletions(-) diff --git a/lfs.c b/lfs.c index 94dfb802..6ef05c0c 100644 --- a/lfs.c +++ b/lfs.c @@ -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: // diff --git a/lfs.h b/lfs.h index 0d282794..6febf28b 100644 --- a/lfs.h +++ b/lfs.h @@ -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;