Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support more compliation targets (including WebAssembly, no_std, and miri) #11

Merged
merged 3 commits into from
Oct 29, 2022

Conversation

robamler
Copy link
Contributor

@robamler robamler commented Mar 9, 2022

I'm curious if you'd be open to considering a pair of related pull requests to both special and probability that make both libraries more portable by adding some cargo "features". I think the functionality provided by both special and probability is so fundamental that both crates would be very useful on many platforms (e.g., I currently have two students who're working on a project that will eventually use probability from WebAssembly).

The downside of supporting these additional compilation targets is that it will require an extra round of testing for each future release (in addition to running cargo test, one would also have to run cargo test --no-default-features).

This PR is split into two commits that add declarations for features system_math and std, respectively, to Cargo.toml. Both features are activated by default to make the changes both backward and forward compatible, but they can be deactivated by downstream crates:

  • Deactivating the feature system_math replaces special's dependency on the system "libm" C library with a re-implementation in pure rust. This allows special to be used on compilation targets that don't have a system "libm" C library, such as WebAssembly.
  • Deactivating the feature std allows applications that use special to be compiled in no_std mode (e.g., for microcontrollers).

A positive side effect of these changes is that they make it possible to run miri on this crate and potentially on downstream crates (miri is a tool to detect undefined behavior in unsafe code): run cargo +nightly miri test --no-default-features

The companion PR for probability is at stainless-steel/probability#27.

The `special` crate current has to be linked with the system `libm`
C library, which is not available on all targets (e.g., when compiling
to WebAssembly). This commit resolves the issue in a backward compatible
way by allowing crates that depend on `special` to *optionally* replace
`special`'s dependency on the system `libm` C library with a pure rust
reimplementation provided in the `libm` rust crate.

This commit adds a cargo feature "system_math" that is activated by
default. As long as a crate that depends on `special` doesn't explicitly
deactivate the "system_math" feature, no behavior changes, and `special`
will still be linked against the system `libm` C library. But if a crate
that uses `special` deactivates the "system_math" feature (see blow),
then `special` will use the `libm` rust crate instead.

In addition to enabling support for WebAssembly, removing this FFI
dependency also makes it possible to now use miri to test for undefined
behavior in `special` and in crates that depend on `special`:
`cargo +nightly miri test --no-default-features`

Unfortunately, benchmarks indicate that the `libm` rust crate is
slightly slower than the `libm` C library, which is why this commit
still uses the system `libm` C library as long as the "std" feature is
activated (which is the default).

**How users can deactivate the "system_math" feature:**

Application authors can deactivate the "system_math" feature by adding
the following to their `Cargo.toml`:

```
[dependencies]
special = {version = "x.y.z", default-features = false}
```

Library authors might rather want to pass this decision on to their
users. To do so, add a new default feature called "system_math" to your
library which, when activated, activates `special`'s "system_math"
feature:

```
[features]
default = ["system_math"]
system_math = ["special/system_math"]

[dependencies]
special = {version = "x.y.z", default-features = false}
```

**Rationale for activating the "system_math" feature by default:**

Making the "system_math" feature active by default makes this change
backward compatible, i.e., existing users of the `special` crate won't
have to change anything in their `Cargo.toml`. Alternatively to
introducing a default feature that turns the system dependency *on*, we
could instead introduce a non-default feature that turns the system
dependency *off*. However, this might not be forward-compatible since
cargo features have to be *additive* (i.e., adding a cargo feature must
never remove functionality), and a future version of `special` might
provide additional features that are only available if a system `libm`
C library is available. See the following part in the cargo reference
(in particular the discussion of `no_std` vs `std`, which is related):
<https://doc.rust-lang.org/cargo/reference/features.html#feature-unification>
Adds a cargo features "std" that is active by default and that, when
deactivated by a downstream crate, ensures that `special` can be
compiled to a `no_std` target (e.g., a microcontroller). This is the
recommended way to optionally support `no_std`, see cargo reference:
<https://doc.rust-lang.org/cargo/reference/features.html#feature-unification>

**Changes for users:**

This change is backward compatible. Existing application authors don't
have to change anything. Existing library authors aren't strictly forced
to change anything either, but they should consider passing on passing
the optional `no_std` compatibility on to their users. If you can make
your library or at least parts of it `no_std` compatible, then:

* Add the line `#![cfg_attr(not(feature = "std"), no_std)]` to your
  library's `lib.rs`;

* Avoid using any functionality from the `std` part of the rust standard
  library; use equivalent definitions from `core` or `alloc` instead
  where available.

* If some features or tests of your library really require the `std`
  part of the standard library, then annotate them with the compiler
  directive `#[cfg(not(std))]` to exclude them from builds for `no_std`
  systems.

* Add the following (or similar) to your `Cargo.toml`:

```
[features]
default = ["std", "system_math"]
std = ["special/std"]
system_math = ["special/system_math"]

[dependencies]
special = {version = "x.y.z", default-features = false}
```

**Changes for contributors:**

When adding new code to a library with a default "std" feature, it is
easy to inadvertently break the library for `no_std` users without
noticing it. This can be prevented by regularly testing without the
"std" feature by running `cargo check`, `cargo test`, and
`cargo +nightly bench` with the `--no-default-features` flag.

To make sure that every new functionality works in `no_std` mode, avoid
using the `std` crate when possible. This is usually easy for low-level
libraries like `special` since most relevant functionality of the `std`
crate is also available in `core` or `alloc`. If you really do need the
`std` crate (e.g., for unit tests that write to a file, etc.), then you
can add the `#[cfg(feature = "std")]` gate. E.g., for a unit test:
[dependencies]
libc = "0.2"
libc = {version = "0.2", optional = true}
libm = "0.2.2" # Only used if default feature "libc" is *not* activated.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Should actually read: # Only used if default feature "system_math" is *not* activated.

@IvanUkhov
Copy link
Member

IvanUkhov commented Jul 17, 2022

I apologize for the late response. It was too much to process. Hence, the procrastination.

It would be a very useful addition! I have a few questions to begin with.

  1. If there is a re-implementation in pure Rust, why keeping the dependency on libc at all? Is it because of totally different performance profiles?
  2. If no_std allows one to implement the current function set, why not getting rid of std all together?

@robamler
Copy link
Contributor Author

Sorry for the late response on my side.

I'd be happy to remove the feature gates if you'd prefer that. My motivation for these gates was not so much driven by performance but more by backward and forward compatibility. The following considerations were on my mind:

  • backward compatibility:
    • (my main motivation for introducing feature gates:) Changing the underlying implementation of special mathematical functions will likely lead to slightly different results of calculations since different implementations might use varying approximations. This will break any downstream crates that require exactly reproducible calculations. For example, I maintain the constriction crate, for which this would be an issue. A more common place where even tiny approximation errors might become an issue could be unit tests in downstream crates if they check results of example calculations in a naive way (those unit tests could be fixed, though).
    • Since the proposed changes have a fairly "global" effect I can't say with certainty that all functionality really still works with both default features turned off. All unit tests pass with the two default features turned off, but there might be untested functionality that breaks (it's unlikely though since it's hard to think of an error in this context that wouldn't already show up at compilation time).
  • forward compatibility:
    • Even though all current functionality still seems to work with the two default features turned off, some new feature that you might want to add in a future version of special or probability might not. Removing the proposed feature gates would therefore lock you in to a less capable programming environment. By contrast, keeping the proposed feature gates with the proposed defaults would make sure that nothing changes in the "default" case. This would allow you to add new functionality even if it doesn't work in no_std mode or if it requires a system libm library. Such new functionality then couldn't be used by the few downstream crates that turn off the std or system_math feature, but most users will be able to get the new functionality.

Of course, removing the feature gates would have the advantage of not splitting the ecosystem, so I'd be happy either way.

Concerning Performance

I added a commit with benchmarking code for four affected functions. Below are my results when I run the benchmark 100 times with default settings (blue) and 100 times with the --no-default-features switch (orange); these 200 runs were interleaved randomly.

benches

It's not a clear picture: the system math library is faster (as one would expect) for 3 out of 4 functions, but the situation is reversed for ln_gamma(). From a backward-compatibility standpoint, silently switching out the system libm for its reimplementation in Rust would lead to a performance regression, at least for some code.

(The above benchmark results might depend strongly on the operating system that provides the system math library; above results were obtained on Ubuntu 22.04.1 with libc version 2.35 and Rust version 1.65.0-nightly (ce40690a5 2022-08-09)).

@IvanUkhov
Copy link
Member

Thank you for the answers! I would prefer to have one implementation, and it should be a Rust one whenever possible. As for the backward and forward compatibilities, I would not be afraid to break things at this stage. It is better to have clean code in a state we want it to be in instead of maintaining a bag of special cases.

If you have time, please go ahead and reduce it to libm with no_std. Let us please also not extend the functionality of this crate within this PR. (I am referring to FloatExt.) It can be discussed separately.

@IvanUkhov IvanUkhov merged commit 3d0480a into stainless-steel:master Oct 29, 2022
@IvanUkhov
Copy link
Member

I have now updated the crate with respect to my previous comment. Thank you for your PR!

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

Successfully merging this pull request may close these issues.

2 participants