From ba0f82c3562dd16f55079e0c4591cf3ed8be874d Mon Sep 17 00:00:00 2001 From: hinto-janai Date: Sat, 10 Feb 2024 18:19:12 -0500 Subject: [PATCH] helper: use `crossbeam::atomic::AtomicCell` for atomic floats (#56) * cargo.toml: add `crossbeam` * helper: use `crossbeam::atomic::AtomicCell` for `AtomicF(32|64)` * helper: atomic docs --- Cargo.lock | 10 + Cargo.toml | 2 +- helper/Cargo.toml | 9 +- helper/src/atomic.rs | 511 +++++-------------------------------------- 4 files changed, 65 insertions(+), 467 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c434aed..49fd30c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -369,6 +369,15 @@ dependencies = [ "libc", ] +[[package]] +name = "crossbeam" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1137cd7e7fc0fb5d3c5a8678be38ec56e819125d8d7907411fe24ccb943faca8" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-deque" version = "0.8.5" @@ -466,6 +475,7 @@ name = "cuprate-helper" version = "0.1.0" dependencies = [ "chrono", + "crossbeam", "futures", "libc", "rayon", diff --git a/Cargo.toml b/Cargo.toml index 3989579..4b333c8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,6 +41,7 @@ bytes = { version = "1.5.0", default-features = false } clap = { version = "4.4.7", default-features = false } chrono = { version = "0.4.31", default-features = false } crypto-bigint = { version = "0.5.5", default-features = false } +crossbeam = { version = "0.8.4", default-features = false } curve25519-dalek = { version = "4.1.1", default-features = false } dalek-ff-group = { git = "https://github.com/Cuprate/serai.git", rev = "a59966b", default-features = false } dirs = { version = "5.0.1", default-features = false } @@ -74,7 +75,6 @@ proptest-derive = { version = "0.4.0" } ## TODO: ## Potential dependencies. # arc-swap = { version = "1.6.0" } # Atomically swappable Arc | https://github.com/vorner/arc-swap -# crossbeam = { version = "0.8.2" } # Channels, concurrent primitives | https://github.com/crossbeam-rs/crossbeam # itoa = { version = "1.0.9" } # Fast integer to string formatting | https://github.com/dtolnay/itoa # notify = { version = "6.1.1" } # Filesystem watching | https://github.com/notify-rs/notify # once_cell = { version = "1.18.0" } # Lazy/one-time initialization | https://github.com/matklad/once_cell diff --git a/helper/Cargo.toml b/helper/Cargo.toml index d74ca1c..34feb99 100644 --- a/helper/Cargo.toml +++ b/helper/Cargo.toml @@ -12,16 +12,17 @@ repository = "https://github.com/Cuprate/cuprate/tree/main/consensus" # All features on by default. default = ["std", "atomic", "asynch", "num", "time", "thread"] std = [] -atomic = [] +atomic = ["dep:crossbeam"] asynch = ["dep:futures", "dep:rayon"] num = [] time = ["dep:chrono", "std"] thread = ["std", "dep:target_os_lib"] [dependencies] -chrono = { workspace = true, optional = true, features = ["std", "clock"] } -futures = { workspace = true, optional = true, features = ["std"] } -rayon = { workspace = true, optional = true } +crossbeam = { workspace = true, optional = true } +chrono = { workspace = true, optional = true, features = ["std", "clock"] } +futures = { workspace = true, optional = true, features = ["std"] } +rayon = { workspace = true, optional = true } # This is kinda a stupid work around. # [thread] needs to activate one of these libs (windows|libc) diff --git a/helper/src/atomic.rs b/helper/src/atomic.rs index b563eb1..f253737 100644 --- a/helper/src/atomic.rs +++ b/helper/src/atomic.rs @@ -3,488 +3,75 @@ //! `#[no_std]` compatible. //---------------------------------------------------------------------------------------------------- Use -use core::sync::atomic::{AtomicU32, AtomicU64, Ordering}; +use crossbeam::atomic::AtomicCell; + +#[allow(unused_imports)] // docs +use std::sync::atomic::{Ordering, Ordering::Acquire, Ordering::Release}; //---------------------------------------------------------------------------------------------------- Atomic Float -// An AtomicF(32|64) implementation. -// -// This internally uses [AtomicU(32|64)], where the -// u(32|64) is the bit pattern of the internal float. -// -// This uses [.to_bits()] and [from_bits()] to -// convert between actual floats, and the bit -// representations for storage. -// -// Using `UnsafeCell` is also viable, -// and would allow for a `const fn new(f: float) -> Self` -// except that becomes problematic with NaN's and infinites: -// - -// - -// -// This is most likely safe(?) but... instead of risking UB, -// this just uses the Atomic unsigned integer as the inner -// type instead of transmuting from `UnsafeCell`. -// -// This creates the types: -// - `AtomicF32` -// - `AtomicF64` -// -// Originally taken from: -// -macro_rules! impl_atomic_f { - ( - $atomic_float:ident, // Name of the new float type - $atomic_float_lit:literal, // Literal name of new float type - $float:ident, // The target float (f32/f64) - $unsigned:ident, // The underlying unsigned type - $atomic_unsigned:ident, // The underlying unsigned atomic type - $bits_0:literal, // Bit pattern for 0.0 - $bits_025:literal, // Bit pattern for 0.25 - $bits_050:literal, // Bit pattern for 0.50 - $bits_075:literal, // Bit pattern for 0.75 - $bits_1:literal, // Bit pattern for 1.0 - ) => { - /// An atomic float. - /// - /// ## Portability - /// [Quoting the std library: ]( - /// "See from_bits for some discussion of the portability of this operation (there are almost no issues)." - /// - /// ## Compile-time failure - /// This internal functions `std` uses will panic _at compile time_ - /// if the bit transmutation operations it uses are not available - /// on the build target, aka, if it compiles we're probably safe. - #[repr(transparent)] - #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] - #[cfg_attr(feature = "bincode", derive(bincode::Encode, bincode::Decode))] - pub struct $atomic_float($atomic_unsigned); +/// Compile-time assertion that our floats are +/// lock-free for the target we're building for. +const _: () = { + assert!( + AtomicCell::::is_lock_free(), + "32-bit atomics are not supported on this build target." + ); - impl $atomic_float { - /// Representation of `0.0` as bits, can be inputted into [`Self::from_bits`]. - pub const BITS_0: $unsigned = $bits_0; - /// Representation of `0.25` as bits, can be inputted into [`Self::from_bits`]. - pub const BITS_0_25: $unsigned = $bits_025; - /// Representation of `0.50` as bits, can be inputted into [`Self::from_bits`]. - pub const BITS_0_50: $unsigned = $bits_050; - /// Representation of `0.75` as bits, can be inputted into [`Self::from_bits`]. - pub const BITS_0_75: $unsigned = $bits_075; - /// Representation of `1.0` as bits, can be inputted into [`Self::from_bits`]. - pub const BITS_0_100: $unsigned = $bits_1; + assert!( + AtomicCell::::is_lock_free(), + "64-bit atomics are not supported on this build target." + ); +}; - #[allow(clippy::declare_interior_mutable_const)] - // FIXME: - // Seems like `std` internals has some unstable cfg options that - // allow interior mutable consts to be defined without clippy complaining: - // . - // - /// `0.0`, returned by [`Self::default`]. - pub const DEFAULT: Self = Self($atomic_unsigned::new($bits_0)); +// SOMEDAY: use a custom float that implements `Eq` +// so that `compare_exchange()`, `fetch_*()` work. - #[inline] - /// Create a new atomic float. - /// - /// Equivalent to - pub fn new(f: $float) -> Self { - // FIXME: Update to const when available. - // - // - // `transmute()` here would be safe (`to_bits()` is doing this) - // although checking for NaN's and infinites are non-`const`... - // so we can't can't `transmute()` even though it would allow - // this function to be `const`. - Self($atomic_unsigned::new(f.to_bits())) - } +/// An atomic [`f32`]. +/// +/// This is an alias for +/// [`crossbeam::atomic::AtomicCell`](https://docs.rs/crossbeam/latest/crossbeam/atomic/struct.AtomicCell.html). +/// +/// Note that there are no [`Ordering`] parameters, +/// atomic loads use [`Acquire`], +/// and atomic stores use [`Release`]. +pub type AtomicF32 = AtomicCell; - #[inline] - /// Equivalent to - pub fn into_inner(self) -> $float { - $float::from_bits(self.0.into_inner()) - } - - #[inline] - /// Create a new atomic float, from the unsigned bit representation. - pub const fn from_bits(bits: $unsigned) -> Self { - Self($atomic_unsigned::new(bits)) - } - - #[inline] - /// Store a float inside the atomic. - /// - /// Equivalent to - pub fn store(&self, f: $float, ordering: Ordering) { - self.0.store(f.to_bits(), ordering); - } - - #[inline] - /// Store a bit representation of a float inside the atomic. - pub fn store_bits(&self, bits: $unsigned, ordering: Ordering) { - self.0.store(bits, ordering); - } - - #[inline] - /// Load the internal float from the atomic. - /// - /// Equivalent to - pub fn load(&self, ordering: Ordering) -> $float { - // FIXME: Update to const when available. - // - $float::from_bits(self.0.load(ordering)) - } - - #[inline] - /// Load the internal bit representation of the float from the atomic. - pub fn load_bits(&self, ordering: Ordering) -> $unsigned { - self.0.load(ordering) - } - - #[inline] - /// Equivalent to - pub fn swap(&self, val: $float, ordering: Ordering) -> $float { - $float::from_bits(self.0.swap($float::to_bits(val), ordering)) - } - - #[inline] - /// Equivalent to - pub fn compare_exchange( - &self, - current: $float, - new: $float, - success: Ordering, - failure: Ordering, - ) -> Result<$float, $float> { - match self - .0 - .compare_exchange(current.to_bits(), new.to_bits(), success, failure) - { - Ok(b) => Ok($float::from_bits(b)), - Err(b) => Err($float::from_bits(b)), - } - } - - #[inline] - /// Equivalent to - pub fn compare_exchange_weak( - &self, - current: $float, - new: $float, - success: Ordering, - failure: Ordering, - ) -> Result<$float, $float> { - match self.0.compare_exchange_weak( - current.to_bits(), - new.to_bits(), - success, - failure, - ) { - Ok(b) => Ok($float::from_bits(b)), - Err(b) => Err($float::from_bits(b)), - } - } - - //------------------------------------------------------------------ fetch_*() - // These are tricky to implement because we must - // operate on the _numerical_ value and not the - // bit representations. - // - // This means using some type of CAS, - // which comes with the regular tradeoffs... - - // The (private) function using CAS to implement `fetch_*()` operations. - // - // This is function body used in all the below `fetch_*()` functions. - fn fetch_update_unwrap(&self, ordering: Ordering, mut update: F) -> $float - where - F: FnMut($float) -> $float, - { - // Since it's a CAS, we need a second ordering for failures, - // this will take the user input and return an appropriate order. - let second_order = match ordering { - Ordering::Release | Ordering::Relaxed => Ordering::Relaxed, - Ordering::Acquire | Ordering::AcqRel => Ordering::Acquire, - Ordering::SeqCst => Ordering::SeqCst, - // Ordering is #[non_exhaustive], so we must do this. - ordering => ordering, - }; - - // SAFETY: - // unwrap is safe since `fetch_update()` only panics - // if the closure we pass it returns `None`. - // As seen below, we're passing a `Some`. - // - // - self.fetch_update(ordering, second_order, |f| Some(update(f))) - .unwrap() - } - - #[inline] - /// This function is implemented with [`Self::fetch_update`], and is not 100% equivalent to - /// . - /// - /// In particular, this method will not circumvent the [ABA Problem](https://en.wikipedia.org/wiki/ABA_problem). - /// - /// Other than this not actually being atomic, all other behaviors are the same. - pub fn fetch_add(&self, val: $float, order: Ordering) -> $float { - self.fetch_update_unwrap(order, |f| f + val) - } - - #[inline] - /// This function is implemented with [`Self::fetch_update`], and is not 100% equivalent to - /// . - /// - /// In particular, this method will not circumvent the [ABA Problem](https://en.wikipedia.org/wiki/ABA_problem). - /// - /// Other than this not actually being atomic, all other behaviors are the same. - pub fn fetch_sub(&self, val: $float, order: Ordering) -> $float { - self.fetch_update_unwrap(order, |f| f - val) - } - - #[inline] - /// This function is implemented with [`Self::fetch_update`], and is not 100% equivalent to - /// . - /// - /// In particular, this method will not circumvent the [ABA Problem](https://en.wikipedia.org/wiki/ABA_problem). - /// - /// Other than this not actually being atomic, all other behaviors are the same. - pub fn fetch_max(&self, val: $float, order: Ordering) -> $float { - self.fetch_update_unwrap(order, |f| f.max(val)) - } - - #[inline] - /// This function is implemented with [`Self::fetch_update`], and is not 100% equivalent to - /// . - /// - /// In particular, this method will not circumvent the [ABA Problem](https://en.wikipedia.org/wiki/ABA_problem). - /// - /// Other than this not actually being atomic, all other behaviors are the same. - pub fn fetch_min(&self, val: $float, order: Ordering) -> $float { - self.fetch_update_unwrap(order, |f| f.min(val)) - } - - #[inline] - /// Equivalent to - pub fn fetch_update( - &self, - set_order: Ordering, - fetch_order: Ordering, - mut f: F, - ) -> Result<$float, $float> - where - F: FnMut($float) -> Option<$float>, - { - // Very unreadable closure... - // - // Basically this is converting: - // `f(f32) -> Option` into `f(u32) -> Option` - // so the internal atomic `fetch_update` can work. - let f = |bits: $unsigned| f($float::from_bits(bits)).map(|f| $float::to_bits(f)); - - match self.0.fetch_update(set_order, fetch_order, f) { - Ok(b) => Ok($float::from_bits(b)), - Err(b) => Err($float::from_bits(b)), - } - } - - #[inline] - /// Set the internal float from the atomic, using [`Ordering::Release`]. - pub fn set(&self, f: $float) { - self.store(f, Ordering::Release); - } - - #[inline] - /// Get the internal float from the atomic, using [`Ordering::Acquire`]. - pub fn get(&self) -> $float { - self.load(Ordering::Acquire) - } - } - - impl From<$float> for $atomic_float { - /// Calls [`Self::new`] - fn from(float: $float) -> Self { - Self::new(float) - } - } - - impl Default for $atomic_float { - /// Returns `0.0`. - fn default() -> Self { - Self::DEFAULT - } - } - - impl std::fmt::Debug for $atomic_float { - /// This prints the internal float value, using [`Ordering::Acquire`]. - /// - /// # Panics - /// This panics on NaN or subnormal float inputs. - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_tuple($atomic_float_lit) - .field(&self.0.load(Ordering::Acquire)) - .finish() - } - } - }; -} - -impl_atomic_f! { - AtomicF64, - "AtomicF64", - f64, - u64, - AtomicU64, - 0, - 4598175219545276416, - 4602678819172646912, - 4604930618986332160, - 4607182418800017408, -} - -impl_atomic_f! { - AtomicF32, - "AtomicF32", - f32, - u32, - AtomicU32, - 0, - 1048576000, - 1056964608, - 1061158912, - 1065353216, -} +/// An atomic [`f64`]. +/// +/// This is an alias for +/// [`crossbeam::atomic::AtomicCell`](https://docs.rs/crossbeam/latest/crossbeam/atomic/struct.AtomicCell.html). +/// +/// Note that there are no [`Ordering`] parameters, +/// atomic loads use [`Acquire`], +/// and atomic stores use [`Release`]. +pub type AtomicF64 = AtomicCell; //---------------------------------------------------------------------------------------------------- TESTS #[cfg(test)] mod tests { use super::*; - // These tests come in pairs, `f32|f64`. - // - // If changing one, update the other as well. - // - // `macro_rules!()` + `paste!()` could do this automatically, - // but that might be more trouble than it's worth... - #[test] - // Tests the varying fetch, swap, and compare functions. - fn f32_functions() { + // Tests `AtomicF32`. + fn f32() { let float = AtomicF32::new(5.0); - let ordering = Ordering::SeqCst; // Loads/Stores - assert_eq!(float.swap(1.0, ordering), 5.0); - assert_eq!(float.load(ordering), 1.0); - float.store(2.0, ordering); - assert_eq!(float.load(ordering), 2.0); - - // CAS - assert_eq!( - float.compare_exchange(2.0, 5.0, ordering, ordering), - Ok(2.0) - ); - assert_eq!( - float.fetch_update(ordering, ordering, |f| Some(f * 3.0)), - Ok(5.0) - ); - assert_eq!(float.get(), 15.0); - loop { - if let Ok(float) = float.compare_exchange_weak(15.0, 2.0, ordering, ordering) { - assert_eq!(float, 15.0); - break; - } - } - - // `fetch_*()` functions - assert_eq!(float.fetch_add(1.0, ordering), 2.0); - assert_eq!(float.fetch_sub(1.0, ordering), 3.0); - assert_eq!(float.fetch_max(5.0, ordering), 2.0); - assert_eq!(float.fetch_min(0.0, ordering), 5.0); + assert_eq!(float.swap(1.0), 5.0); + assert_eq!(float.load(), 1.0); + float.store(2.0); + assert_eq!(float.load(), 2.0); } #[test] - fn f64_functions() { + // Tests `AtomicF64`. + fn f64() { let float = AtomicF64::new(5.0); - let ordering = Ordering::SeqCst; - assert_eq!(float.swap(1.0, ordering), 5.0); - assert_eq!(float.load(ordering), 1.0); - float.store(2.0, ordering); - assert_eq!(float.load(ordering), 2.0); - - assert_eq!( - float.compare_exchange(2.0, 5.0, ordering, ordering), - Ok(2.0) - ); - assert_eq!( - float.fetch_update(ordering, ordering, |f| Some(f * 3.0)), - Ok(5.0) - ); - assert_eq!(float.get(), 15.0); - - loop { - if let Ok(float) = float.compare_exchange_weak(15.0, 2.0, ordering, ordering) { - assert_eq!(float, 15.0); - break; - } - } - - assert_eq!(float.fetch_add(1.0, ordering), 2.0); - assert_eq!(float.fetch_sub(1.0, ordering), 3.0); - assert_eq!(float.fetch_max(5.0, ordering), 2.0); - assert_eq!(float.fetch_min(0.0, ordering), 5.0); - } - - #[test] - fn f32_bits() { - assert_eq!(AtomicF32::default().get(), 0.00); - assert_eq!(AtomicF32::from_bits(AtomicF32::BITS_0).get(), 0.00); - assert_eq!(AtomicF32::from_bits(AtomicF32::BITS_0_25).get(), 0.25); - assert_eq!(AtomicF32::from_bits(AtomicF32::BITS_0_50).get(), 0.50); - assert_eq!(AtomicF32::from_bits(AtomicF32::BITS_0_75).get(), 0.75); - assert_eq!(AtomicF32::from_bits(AtomicF32::BITS_0_100).get(), 1.00); - } - - #[test] - fn f64_bits() { - assert_eq!(AtomicF64::default().get(), 0.00); - assert_eq!(AtomicF64::from_bits(AtomicF64::BITS_0).get(), 0.00); - assert_eq!(AtomicF64::from_bits(AtomicF64::BITS_0_25).get(), 0.25); - assert_eq!(AtomicF64::from_bits(AtomicF64::BITS_0_50).get(), 0.50); - assert_eq!(AtomicF64::from_bits(AtomicF64::BITS_0_75).get(), 0.75); - assert_eq!(AtomicF64::from_bits(AtomicF64::BITS_0_100).get(), 1.00); - } - - #[test] - fn f32_0_to_100() { - let mut i = 0.0; - let f = AtomicF32::new(0.0); - while i < 100.0 { - f.set(i); - assert_eq!(f.get(), i); - i += 0.1; - } - } - - #[test] - fn f64_0_to_100() { - let mut i = 0.0; - let f = AtomicF64::new(0.0); - while i < 100.0 { - f.set(i); - assert_eq!(f.get(), i); - i += 0.1; - } - } - - #[test] - fn f32_irregular() { - assert!(AtomicF32::new(f32::NAN).get().is_nan()); - assert_eq!(AtomicF32::new(f32::INFINITY).get(), f32::INFINITY); - assert_eq!(AtomicF32::new(f32::NEG_INFINITY).get(), f32::NEG_INFINITY); - } - - #[test] - fn f64_irregular() { - assert!(AtomicF64::new(f64::NAN).get().is_nan()); - assert_eq!(AtomicF64::new(f64::INFINITY).get(), f64::INFINITY); - assert_eq!(AtomicF64::new(f64::NEG_INFINITY).get(), f64::NEG_INFINITY); + // Loads/Stores + assert_eq!(float.swap(1.0), 5.0); + assert_eq!(float.load(), 1.0); + float.store(2.0); + assert_eq!(float.load(), 2.0); } }