Added ability to bypass rbyd fetch during B-tree lookups

This is an absurd optimization that stems from the observation that the
branch encoding for the inner-rbyds in a B-tree is enough information to
jump directly to the trunk of the rbyd without needing an lfsr_rbyd_fetch.

This results in a pretty ridiculous performance jump from O(m log_m(n/m))
to O(log(m) log_m(n/m)).

If the complexity analysis isn't impressive enough, look at some rough
benchmarking of read operations for 4KiB-block, 1K-entry B-trees:

   12KiB ^     ::  :. :: .: .: :. : .: :. : : .. : : . : .: : : :
         |    .:: .::.::.:: ::.::::::::::::.::::::::.::::::::::::.
         |    : :::':: ::'::'::':: :' :':: :'::::::::': ::::::': :
before   |  ::: ::' :' :' :: :' '' '  ' '' : : : '' ' ' '
         | :::            ''
         |:
      0B :'------------------------------------------------------>

  .17KiB ^               ............:::::::::::::::::::::::::::::
         |   .   .....:::::'''''''''  '         '          '
         |  .::::::::::::
after    |  :':''
         |.::
         .:'
      0B :------------------------------------------------------->
         0                                                      1K

In order for this to work, the branch encoding did need to be tweaked
slightly. Before it stored block+off, now it stores block+trunk where
"trunk" is the offset of the entry point into the rbyd tree. Both off
and trunk are enough info to know when to stop fetching, if necessary,
but trunk allows lookups to jump directly into the branches rbyd tree
without a fetch.

With the change to trunk, lfsr_rbyd_fetch has also be extended to allow
fetching of any internal trunks, not just the last trunk in the commit.
This is very useful for dbgrbyd.py, but doesn't currently have a use in
littlefs itself. But it's at least valuable to have the feature available
in case it does become useful.

Note that two cases still requires the slower O(m log_m(n/m)) lookup
with lfsr_rbyd_fetch:

1. Name lookups, since we currently use a linear-search O(m) to find names.

2. Validating B-tree rbyd's, which requires a linear fetch O(m) to
   validate the checksums. We will need to do this at least once
   after mount.

It's also worth mentioning this will likely have a large impact on B-tree
traversal speed. Which is huge as I am expecting B-tree traversal to be
the main bottleneck once garbage-collection (or its replacement) is
involved.
This commit is contained in:
Christopher Haster
2023-04-09 02:21:02 -05:00
parent ed8d8c0c24
commit a511696bad
7 changed files with 1304 additions and 1130 deletions
+40 -20
View File
@@ -330,12 +330,16 @@ typedef struct lfs_cache {
uint8_t *buffer;
} lfs_cache_t;
// TODO do we get ram savings with a lfsr_rorbyd_t substruct? need to measure
typedef struct lfsr_rbyd {
// note this lines up with weight in lfsr_btree_t
lfs_size_t weight;
lfs_block_t block;
// off=0, trunk=0 => not yet committed
// off=0, trunk>0 => not yet fetched
// off=block_size => rbyd not erased/needs compaction
lfs_off_t off;
lfs_off_t trunk;
lfs_size_t weight;
uint32_t rev;
uint32_t crc;
} lfsr_rbyd_t;
@@ -358,27 +362,43 @@ typedef struct lfsr_rbyd {
// - block addresses => 1 leb128 => 5 bytes (worst case)
#define LFSR_BTREE_INLINE_SIZE 5
typedef struct lfsr_branch {
lfs_block_t block;
lfs_size_t limit;
} lfsr_branch_t;
typedef struct lfsr_btree {
lfs_size_t weight;
// TODO do we need full tag actually? this fits in a byte?
lfsr_tag_t tag;
// how can we take advantage of byte packing with union alignment?
union {
struct {
uint8_t size;
uint8_t buf[LFSR_BTREE_INLINE_SIZE];
} inlined;
// if we're not inlined, point to the trunk rbyd block of the btree
lfsr_branch_t trunk;
} u;
typedef union lfsr_btree {
// note this lines up with weight in lfsr_rbyd_t
//
// weight=0 => null btree
// weight<0 => inlined btree
// weight>0 => normal btree
lfs_ssize_t weight;
lfsr_rbyd_t root;
struct {
lfs_ssize_t weight;
lfsr_tag_t tag;
uint16_t len;
uint8_t buf[LFSR_BTREE_INLINE_SIZE];
} inlined;
} lfsr_btree_t;
//typedef struct lfsr_branch {
// lfs_block_t block;
// lfs_size_t limit;
//} lfsr_branch_t;
//
//typedef struct lfsr_btree {
// lfs_size_t weight;
// // TODO do we need full tag actually? this fits in a byte?
// lfsr_tag_t tag;
// // how can we take advantage of byte packing with union alignment?
// union {
// struct {
// uint8_t size;
// uint8_t buf[LFSR_BTREE_INLINE_SIZE];
// } inlined;
//
// // if we're not inlined, point to the trunk rbyd block of the btree
// lfsr_branch_t trunk;
// } u;
//} lfsr_btree_t;
typedef struct lfs_mdir {
lfs_block_t pair[2];
uint32_t rev;