vorner / arc-swap

Support atomic operations on Arc itself
Apache License 2.0
777 stars 31 forks source link

no_std support, more minimal and less intrusive version #93

Closed Alexis211 closed 8 months ago

Alexis211 commented 9 months ago

This PR proposes a non-invasive, relatively minimal implementation of no_std support for the arc-swap crate. The aim of this PR is to enable no_std without much effort for specific use-cases such as embedded or os dev, which are often already using a nightly compiler anyways, without disturbing the existing functionning of the crate. I saw the previous PR for no_std support (#56) which was not merged due to lack of activity. If possible I'd like to do the necessary work so that we can have no_std support in arc-swap soon.

To answer questions from the other thread (PR #56):

First of all, how lock-free is the library for thread-local storage and initialization? I think it would be OK for it to have some locks on the first access in each thread, or on thread creation. But it would kind of beat the whole purpose of arc-swap if there was a lock in each access. Did you have a chance to study the documentation or implementation?

In this PR, we are using only the experimental #[thread_local] tag of the nightly compiler. This compiles down to LLVM thread local primitives, which are supposedly one of the fastest possible underlying implementation for std's thread_local! macro, so no performance penalty on that side is to be expected. Custom implementations can also be made easily without locks. The thread local variable itself that is used by arc-swap is a LazyCell which is not Sync, so there is no mutex or synchronizaton penalty on initialization.

Without enabling the new feature, all previous features need to compile as previously. Specifically, with no features enabled, it must be possible to compile even with 1.31.0.

This is the case with this PR, albeit the lowest supported version is 1.38.0, which is not related to changes in this PR. Versions <=1.37.0 failed with the following error, which was already the case on the master branch:

error: failed to parse lock file at: /home/lx/dev/crates/arc-swap/Cargo.lock

Caused by:
  invalid serialized PackageId for key `package.dependencies`

I believe this new feature doesn't have to be incompatible with the weak feature.

Not an issue in this PR.

I also believe the new feature should be compatible with stable rust, not work only on nightly.

Unfortunately, this is not doable easily in the current state of the Rust ecosystem, at least not with a lot of additional effort. This PR is a minimal try to land no_std support in the arc-swap crate as simply as possible, so it naturally comes with some restriction. I tried to have as little restrictions as possible, and basically there are only two conditions for this to be used: use a nightly compiler, and have support for compiler-generated thread local variables.

I'd be glad to have your comments and advice on how we can make this into something that can be merged into arc-swap.

vorner commented 9 months ago

Reading through the changes, I must say I kind of like them. The overall feeling is it is kind of elegant and nice.

I do have few details to discuss, though:

As for the old build… I think you can still build the crate if you delete the lock file. The lock file is not used when the crate is just a dependency.

Alexis211 commented 9 months ago

How far away from stabilization are the features you use? Is it worth waiting for them, or can we rely on them not changing much?

The thread_local feature has been in Nightly for a very long time, it doesn't look like it will be ready for stabilization anytime soon, but it also doesn't look like it is changing a lot. I think this is the main feature that is gating no_std support in our case. See rust-lang/rust#29594

The lazy_cell feature is much more recent (<1 year, see rust-lang/rust#109736), and it looks like the design is still under debate, so maybe it's not a so good idea after all to rely on it. I pushed a new version that achieves the same result using RefCell<Option<_>> instead of LazyCell<_>, it's just a bit less elegant because we need explicit testing for initialization in LocalNode::with, but it works. This should have near-zero impact on performance.

Eventually, it would be nice to use these in normal std build too, if they prove faster. In that regard, the no-std feature flag seems a bit misleading. Maybe something like lazy-cell feature instead? Nevertheless, as long as this is a nightly-only feature, it probably should be marked as such ‒ something like experimental-lazy-cell, so people know the stability guarantee doesn't hold.

I pushed a commit that renames the feature flag experimental-thread-local, but then there is the question of support for std::sync::RwLock. Currently it is disabled when experimental-thread-local is enabled, but I was thinking maybe we could have a feature rwlock that enables the std::sync::RwLock support, and set it as a default feature. What do you think?

I think the CI needs to be adjusted a bit. Currently, it tries to run the no-std in stable / beta, which fails.

I pushed a commit that replaces all occurences of --all-features in the CI scripts with an explcit list of features without experimental-thread-support.

I think adjusting documentation would also be in place.

Yes, I didn't think of it. I'll do this last, when the other questions are resolved.

Alexis211 commented 9 months ago

I think I fixed pretty much everything and added some documentation. Concerning the rw-lock thing, could you check that I did what you were expecting? Especially the conditional compilation in src/strategy/mod.rs.

Thanks :)

vorner commented 8 months ago

Sorry for the delay on my end; this looks good. But the CI tests are currently failing due to some problem in proc-macro2 vs. newest nightly compiler. It is already resolved on master, so a rebase might help.

Also, when I do cargo test --features experimental-thread-local (on nightly compiler, with dependencies updated), it fails. It seems the mod tests sections weren't updated to use alloc::whatever instead of std::whatever and can't find things like Vec or String.

Alexis211 commented 8 months ago

I fixed compilation of the tests when running cargo test --features experimental-thread-local, but unfortunately the tests fail or hang. It looks like the thread local variable is not working correctly, for instance if I focus on the rcu test, I can get outputs like this:

$ cargo test --features experimental-thread-local rcu
    Finished test [unoptimized + debuginfo] target(s) in 0.04s
     Running unittests src/lib.rs (target/debug/deps/arc_swap-834f36c9a23df4c3)

running 1 test
test tests_default::rcu ... FAILED

failures:

---- tests_default::rcu stdout ----
thread '<unnamed>' panicked at src/debt/list.rs:253:47:
already borrowed: BorrowMutError
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
thread '<unnamed>' panicked at src/debt/list.rs:253:47:
already borrowed: BorrowMutError
thread '<unnamed>' panicked at src/debt/list.rs:253:47:
already borrowed: BorrowMutError
thread '<unnamed>' panicked at src/debt/list.rs:253:47:
already borrowed: BorrowMutError
thread '<unnamed>' panicked at src/debt/list.rs:253:47:
already borrowed: BorrowMutError
thread 'tests_default::rcu' panicked at src/lib.rs:1236:1:
called `Result::unwrap()` on an `Err` value: Any { .. }

failures:
    tests_default::rcu

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 31 filtered out; finished in 0.00s

error: test failed, to rerun pass `--lib`

Looking at a stack trace, it looks like there is a recursive function that calls LocalNode::with within itself, leading to double-borrowing. I'll try to investigate if I have time.

Alexis211 commented 8 months ago

Alright, I fixed the tests by using OnceCell instead of RefCell. All tests are working now with --feature experimental-thread-local, I just had to disable the rcu_panic test because panic::catch_unwind is not available in no-std mode.

vorner commented 8 months ago

OK.

Before I go ahead and merge it, do you want to do some cleanup of the history? Because there's a lot of „changes due to review“ commits (which are kind of annoying in future browsing of the history, bisects, etc) instead some kind of per-topic commits. I can either squash it as a whole to a single commit, or you can sort it into something „nice“.

Alexis211 commented 8 months ago

If you don't want to keep the current history, I think it's fine to just squash everything in a single commit.

vorner commented 8 months ago

Thank you, I've done a release.