Skip to content

Conversation

@geky
Copy link
Member

@geky geky commented Nov 29, 2020

This is a big set of config changes that originally came from the idea in this comment: #158 (comment). This would be an API-breaking change (but not disk-breaking), so at the moment I am just looking for feedback.

The big change is the addition of static config, in which defining the relevant defines (LFS_READ_SIZE for example) will override the dynamic config found in lfs_cfg (lfs->cfg->read_size for example). If you only have a single filesystem configuration in your build, this lets the compiler optimize out logic depending on this configuration.

  • Added static configs which allow optimizing out the lfs_cfg and lfs_file_cfg structs 5f885f0

  • Change the *_max configurations to *_limit, since they are slightly different than the *_MAX macros 5f885f0

  • Rename lfs_(mount|format|migrate) -> lfs_(mount|format|migrate)cfg if cfg is provided c44427f

    This matches the existing lfs_file_opencfg.

In addition, this PR contains a number of changes/tweaks/improvements to the config API of littlefs:

  • Renamed the LFS_CONFIG macro to LFS_UTIL to avoid confusion with the lfs_cfg struct ef9ba2d

  • Removed lingering clib includes in lfs.h ef9ba2d

  • Moved error code definitions to lfs_util.h ef9ba2d

  • Used proper stdint definitions in lfs_scmp ef9ba2d

  • Rename *_config structs to *_cfg for consistency a549413

  • Changed callbacks to take user provided context directly a7cdd56

      struct lfs_cfg {
          void *ctx;
    -     int (*read)(const struct lfs_cfg *cfg, ...);
    +     int (*read)(void *ctx, ...);
          ...
  • Changed block device callbacks to use a bd_* prefix 190eb83

      struct lfs_cfg {
    -     void *ctx;
    -     int (*read)(void *ctx, ...);
    +     void *bd_ctx;
    +     int (*bd_read)(void *ctx, ...);
          ...
  • Renamed cache_size -> buffer_size to avoid confusion around the usefulness of >block_size buffer sizes 4990837

I also explored copying the dynamic configuration into the lfs_t struct at runtime. This would have avoided needing to persist the lfs_cfg struct for the lifetime of littlefs. This exists on this branch here:
https://github.com/littlefs-project/littlefs/tree/config-improvements-mut

But I ended up dropping this change as it increased the code size and made the internals quite a bit more complicated in order to support both dynamic and static configurations. More info here: f9324d1. Though I am still open to bringing this in if it's believed to be a better user experience.

Open to thoughts or suggestions.

Supersedes #262


EDITed based on feedback, previously:

The big change is the addition of LFS_STATICCFG, a define which optimizes out the lfs_cfg struct if defined, allowing for smaller builds.

  • Add LFS_STATICCFG to optimize out the lfs_cfg struct c44427f
  • Add LFS_FILE_STATICCFG to optimize out the lfs_file_cfg struct c44427f
  • Rename lfs_(mount|format|migrate) -> lfs_(mount|format|migrate)cfg if cfg is provided c44427f

This matches the existing lfs_file_opencfg.

geky added 10 commits November 28, 2020 18:58
- Changed the name of the LFS_CONFIG macro to LFS_UTIL to avoid
  confusion with the lfs_config struct. This also hints that LFS_UTIL
  is related to lfs_util.h.

  LFS_UTIL allows the user to override lfs_util.h so they can provide
  their own system-level dependencies such as malloc, tracing, builtins,
  stdint definitions, string.h, and others.

- Removed stdlib includes from lfs.h, these should all go through
  lfs_util.h to let users override these definitions if stdlib is
  unavailable on their system.

- Moved error code definitions to lfs_util.h. This lets users override
  the error codes to replace them with their own error codes and avoid
  a translation layer in some situations. Note the error codes must
  still be in the range of a negative int.

- Used proper stdint definitions in lfs_scmp.
As an embedded library, littlefs's configuration straddles two worlds.
In most cases the configuration is usually constant at build time, but
when integrated into OSs, the configuration needs to be dynamically
configurable.

To help with this, littlefs has a separate lfs_config struct that can be
placed into ROM when possible.

But you know what's better than ROM configuration? Truely inlinable
static configuration known at compile-time. In addition to avoiding the
RAM cost, compile-time configuration allows for additional compiler
optimizations, such as constexpr-elimination and removal of unused
code-paths.

So how to enable static configuration?

1. define LFS_STATICCFG
2. implement callbacks as global functions:
   - lfs_read
   - lfs_prog
   - lfs_erase
   - lfs_sync
2. define the now-required constants that configure littlefs:
   - LFS_READ_SIZE
   - LFS_PROG_SIZE
   - LFS_BLOCK_SIZE
   - LFS_BLOCK_COUNT
   - LFS_BLOCK_CYCLES
   - LFS_CACHE_SIZE
   - LFS_LOOKAHEAD_SIZE
   - LFS_READ_BUFFER (optional)
   - LFS_PROG_BUFFER (optional)
   - LFS_LOOKAHEAD_BUFFER (optional)
   - LFS_NAME_MAX (optional)
   - LFS_FILE_MAX (optional)
   - LFS_ATTR_MAX (optional)

Note, there is a separate configuration for the file configuration, this
can be enabled/disabled independently of LFS_STATICCFG. You will likely
want to define this as well if you are looking for the smallest code
size.

In order to avoid a mess of #ifdefs, the internals of littlefs use a
simple macro that redirects to either the dynamic or static config at
compile time:

    #ifdef LFS_STATICCFG
    #define LFS_CFG_READ_SIZE(lfs) ((void)lfs, LFS_READ_SIZE)
    #else
    #define LFS_CFG_READ_SIZE(lfs) lfs->cfg->read_size
    #endif

Unfortunately it does look like there still may be a lot of issues
related to warnings of comparisons against constants... If only C had
a way to ignore warnings on individual statements...

Original idea by apmorton
Since this is already going to be a breaking API change, this renames
structs/variables named _config -> _cfg. This is in order to be
consistent with functions such as lfs_file_opencfg.
This is a style change to make littlefs's callbacks consistent with most
callback declarations found in C. That is, taking in a user-provided
`void*`.

Previously, these callbacks took a pointer to the config struct itself,
which indirectly contained a user provided context, and this gets the
job done, but taking in a callback with a `void*` is arguably more
expected, has a better chance of integrating with C++/OS-specific code,
and is more likely to be optimized out by a clever compiler.

---

As a part of these changes, the geometry for the test bds needed to be
moved into bd specific configuration objects. This is a good change as
it also allows for testing situations where littlefs's geometry does not
match the underlying bd.
I have another branch where I tried exploring inlined config structs
backed by RAM, but this did not work out due to additional RAM and ROM costs.

Changing the bds to follow this was surprisingly annoying, as they had a
lot of shared geometry that was previously in a single shared config
object, and the way that testbd contains either of rambd and filebd made
configuring all three of these a bit complicated.

Ended up settling on a lfs_testbd_cfg that contains optional pointers to
lfs_rambd_cfg and lfs_filebd_cfg. These can be NULL, but only if that
bd goes unused.
This makes the block-device related operations more clearly block-device
related operations, and helps avoid possible conflicts with user
provided `lfs_bd_read/prog/erase` functions in the LFS_STATICCFG mode.
Added internal lineno tracking of generated test files so that test.py
can reset the generated #line directives after a test case. This helps
debug issues with the generated glue code itself, which would previously
end up with invalid/unhelpful lineno information in error messages.

Also changed suffix from .c.t to .tc
This makes littlefs's usage of the term "cache" an entirely internal
concept and hopefully avoids some confusion about the usefulness of
throwing RAM > block_size at these buffers.

The term cache isn't entirely inaccurate, these buffers do act as
single-line caches, however more often the term cache is used to
describe multi-line caches. Maybe this will be added in littlefs's
future, but the code-size cost makes this change not worth the overhead
at the moment.
@geky geky added needs major version breaking functionality only allowed in major versions needs documentation needs documentation and removed needs documentation needs documentation labels Nov 29, 2020
@geky geky mentioned this pull request Nov 29, 2020
@dpgeorge
Copy link
Contributor

Reducing code size and providing static configuration, in general that sounds good to me!

#endif
#ifndef LFS_LOOKAHEAD_SIZE
#error "LFS_STATICCFG requires LFS_LOOKAHEAD_SIZE"
#endif
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Food for thought, but another pattern could have been something like:

struct lfs_config
{
...
#ifndef LFS_STATICCFG_READ_SIZE
    lfs_size_t read_size;
#endif
#ifndef LFS_STATICCFG_WRITE_SIZE
...
};
...
static inline lfs_size_t lfs_cfg_read_size(const struct lfs_config *c)
{
#ifndef LFS_STATICCFG_READ_SIZE
    return c->read_size;
#else
    return LFS_STATICCFG_READ_SIZE;
#endif
}

That would allow for:

  • Partially optimized builds for any configuration. E.g. a user could declare what variables they'd never change for a given static configuration (but maybe would change a different parameter), and this could account for that.
  • You could add min/max checks for the dynamic and/or static configurations that are used across the board. For instance, the block cycles need to be either off, or at least 2 IIRC.
  • Having the functions in the header (not strictly required) allows a single point of entry for users to read a given configuration item regardless of if it's static or dynamic. Right now all of the helpers are placed into the lfs.c file, but it'd be potentially useful for a single point of retrieval.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Granted, doing the above, would require a cfg object to be always passed around to all functions instead of being able to remove it as in this patch; but this could get optimized out with link time optimization.)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I'll have to see what this looks like. Partial optimizations would be nice, though it may be a bit difficult to track down all constant-related warnings.

Granted, doing the above, would require a cfg object to be always passed around to all functions

We could always do a big:

#ifdef LFS_STATICCFG_READ_SIZE && \
        LFS_STATICCFG_PROG_SIZE && \
        LFS_STATICCFG_BLOCK_SIZE && \
        LFS_STATICCFG_BLOCK_COUNT && \
        ...

You could add min/max checks for the dynamic and/or static configurations that are used across the board. For instance, the block cycles need to be either off, or at least 2 IIRC.

Currently these are checked by runtime asserts in lfs_init. Unfortunately the math isn't always decidable at compile-time by C, but the runtime checks can be removed if you define LFS_NO_ASSERTS.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I went ahead and created something since I was curious. See here: tim-nordell@734de59

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interestingly, if the avoidance of division in the eviction logic is applied (#489) in conjunction with a static configuration using powers of 2 for all parameters, we end up with only 1 function performing division with a static configuration: lfs_ctz_index(...)

That function does a non div-by-power-of-2 since it divides by the block size less 2 * 4. It looks like that function calculates something integral to the design of LFS, so the - (2 * 4) can't be removed (to optimize for architectures without a hardware divider).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh! That is an interesting observation.

It doesn't surprise me that lfs_ctz_index gets in the way, it is the most complex math in littlefs. The division is effectively finding the number of blocks to iterate over, but each block has on average 2 pointers, so it doesn't work out that nicely.

Hmm, but the block_size is likely a power-of-two, and 2*4 is a power of two. Is there a shortcut for division by the sum of two powers of two?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pushed up a change to do this 👍 I believe the main difference is I'm using macro names without the LFS_STATICCFG_* prefix.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:) Glad you liked the idea of partials.

Note that using MACROs like that loses the type safetyness of using the static inline functions, but it's probably just a stylistic thing here. There shouldn't be a difference in the compiled size between the two choices if the compiler is doing its job.

Also, I don't know what target C standard this is targeted at (C99, C11, etc), but if it was C11, you can use static asserts (https://en.cppreference.com/w/c/language/_Static_assert) for some of the configuration values statically specified.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that using MACROs like that loses the type safetyness of using the static inline functions

True, though in this case they are just rearranging arguments for another function. Also, if all of the macros are specified, we wouldn't emit the lfs_cfg struct, which would cause a type error if these were inline functions.

Also, I don't know what target C standard this is targeted at (C99, C11, etc), but if it was C11, you can use static asserts

Ah, littlefs sticks to C99, and even this has been a pain point for users. Embedded IDEs move pretty slowly.

Though one nice thing with static_asserts is that you can always have a macro that switches between nothing or a static assert if available.

The main downside would be figuring out what can be static asserted, LFS_ASSERT(LFS_CFG_BLOCK_SIZE(cfg) > LFS_CFG_PROG_SIZE(cfg)) may or may not be static assertable depending on what's defined. And it risks duplicating a lot of these checks.

Instead of 1. defining LFS_STATICCFG and 2. defining all LFS_READ_SIZE,
LFS_PROG_SIZE, etc. Configuration can now be made static by defining
LFS_READ_SIZE, LFS_PROG_SIZE, etc. Thanks to a really large ifdef, if
all configurations are provided, LFS_STATICCFG will be defined and the
RAM cost fully removed.

Additionally, we can remove the ROM cost of each struct cfg member,
allowing code savings when config is only partially defined, which is
perhaps more common.

This moves all of the configuration logic in lfs.h, which has the nice
side effect of keeping all of the decision making in the same location.

The only catch is that we need to differentiate between the cfg->*_max
and *_MAX limits. To do this I've renamed the configuration *_max to
*_limit. Note that these two are slightly different, with *_max
indicating the maximum supported by the current driver, and *_limit
the maximum supported by the specific instance of littlefs. However if
you do define a *_LIMIT, I've added an override for the relevant *_MAX,
since I can't think of a time where you _wouldn't_ want to do that.

---

This also required some tweaks in scripts/test.py in order to populate
the lfs_cfg struct correctly. This happens since the test defines
overlap littlefs's configuration defines. This does change what is being
tested a bit, but hopefully that's not a real issue.

Suggested by tim-nordell-nimbelink
@geky
Copy link
Member Author

geky commented Dec 18, 2020

Pushed up the changes suggested by @tim-nordell-nimbelink

Now, instead of 1. defining LFS_STATICCFG and 2. defining all LFS_READ_SIZE, LFS_PROG_SIZE, etc. Configuration can now be made static by defining LFS_READ_SIZE, LFS_PROG_SIZE, etc. Thanks to a really large ifdef, if all configurations are provided, LFS_STATICCFG will be defined and the RAM cost fully removed.

Additionally, we can remove the ROM cost of each struct cfg member, allowing code savings when config is only partially defined, which is perhaps more common.

This moves all of the configuration logic in lfs.h, which has the nice side effect of keeping all of the decision making in the same location.

The only catch is that we need to differentiate between the cfg->*_max and *_MAX limits. To do this I've renamed the configuration *_max to *_limit. Note that these two are slightly different, with *_max indicating the maximum supported by the current driver, and *_limit the maximum supported by the specific instance of littlefs. However if you do define a *_LIMIT, I've added an override for the relevant *_MAX, since I can't think of a time where you wouldn't want to do that.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement needs major version breaking functionality only allowed in major versions postponed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants