From b41b4fda4317b95cea441f7044caf16713179087 Mon Sep 17 00:00:00 2001 From: Vinzent Steinberg Date: Sun, 6 Sep 2020 23:39:51 +0200 Subject: [PATCH 1/8] SmallRng: Replace PCG algorithm with xoshiro{128,256}++ Due to close correlations of PCG streams (#907) and lack of right-state propagation (#905), the `SmallRng` algorithm is switched to xoshiro{128,256}++. The implementation is taken from the `rand_xoshiro` crate and slightly simplified. Fixes #910. --- Cargo.toml | 3 +- src/rngs/mod.rs | 6 ++ src/rngs/small.rs | 23 ++++---- src/rngs/xoshiro128plusplus.rs | 101 ++++++++++++++++++++++++++++++++ src/rngs/xoshiro256plusplus.rs | 103 +++++++++++++++++++++++++++++++++ 5 files changed, 222 insertions(+), 14 deletions(-) create mode 100644 src/rngs/xoshiro128plusplus.rs create mode 100644 src/rngs/xoshiro256plusplus.rs diff --git a/Cargo.toml b/Cargo.toml index c0d8cba184a..70713bfff73 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,7 +43,7 @@ simd_support = ["packed_simd"] std_rng = ["rand_chacha", "rand_hc"] # Option: enable SmallRng -small_rng = ["rand_pcg"] +small_rng = [] [workspace] members = [ @@ -56,7 +56,6 @@ members = [ [dependencies] rand_core = { path = "rand_core", version = "0.5.1" } -rand_pcg = { path = "rand_pcg", version = "0.2.1", optional = true } log = { version = "0.4.4", optional = true } serde = { version = "1.0.103", features = ["derive"], optional = true } diff --git a/src/rngs/mod.rs b/src/rngs/mod.rs index f866409579b..ba2f81b807d 100644 --- a/src/rngs/mod.rs +++ b/src/rngs/mod.rs @@ -101,7 +101,13 @@ pub mod mock; // Public so we don't export `StepRng` directly, making it a bit // more clear it is intended for testing. + +#[cfg(all(feature = "small_rng", target_pointer_width = "64"))] +mod xoshiro256plusplus; +#[cfg(all(feature = "small_rng", not(target_pointer_width = "64")))] +mod xoshiro128plusplus; #[cfg(feature = "small_rng")] mod small; + #[cfg(feature = "std_rng")] mod std; #[cfg(all(feature = "std", feature = "std_rng"))] pub(crate) mod thread; diff --git a/src/rngs/small.rs b/src/rngs/small.rs index e0b56315935..d9373a34081 100644 --- a/src/rngs/small.rs +++ b/src/rngs/small.rs @@ -10,10 +10,10 @@ use rand_core::{Error, RngCore, SeedableRng}; -#[cfg(all(not(target_os = "emscripten"), target_pointer_width = "64"))] -type Rng = rand_pcg::Pcg64Mcg; -#[cfg(not(all(not(target_os = "emscripten"), target_pointer_width = "64")))] -type Rng = rand_pcg::Pcg32; +#[cfg(target_pointer_width = "64")] +type Rng = super::xoshiro256plusplus::Xoshiro256PlusPlus; +#[cfg(not(target_pointer_width = "64"))] +type Rng = super::xoshiro128plusplus::Xoshiro128PlusPlus; /// A small-state, fast non-crypto PRNG /// @@ -25,15 +25,14 @@ type Rng = rand_pcg::Pcg32; /// The algorithm is deterministic but should not be considered reproducible /// due to dependence on platform and possible replacement in future /// library versions. For a reproducible generator, use a named PRNG from an -/// external crate, e.g. [rand_pcg] or [rand_chacha]. +/// external crate, e.g. [rand_xoshiro] or [rand_chacha]. /// Refer also to [The Book](https://rust-random.github.io/book/guide-rngs.html). /// -/// The PRNG algorithm in `SmallRng` is chosen to be -/// efficient on the current platform, without consideration for cryptography -/// or security. The size of its state is much smaller than [`StdRng`]. -/// The current algorithm is [`Pcg64Mcg`](rand_pcg::Pcg64Mcg) on 64-bit -/// platforms and [`Pcg32`](rand_pcg::Pcg32) on 32-bit platforms. Both are -/// implemented by the [rand_pcg] crate. +/// The PRNG algorithm in `SmallRng` is chosen to be efficient on the current +/// platform, without consideration for cryptography or security. The size of +/// its state is much smaller than [`StdRng`]. The current algorithm is +/// `Xoshiro256PlusPlus` on 64-bit platforms and `Xoshiro128PlusPlus` on 32-bit +/// platforms. Both are implemented by the [rand_xoshiro] crate. /// /// # Examples /// @@ -69,7 +68,7 @@ type Rng = rand_pcg::Pcg32; /// [`StdRng`]: crate::rngs::StdRng /// [`thread_rng`]: crate::thread_rng /// [rand_chacha]: https://crates.io/crates/rand_chacha -/// [rand_pcg]: https://crates.io/crates/rand_pcg +/// [rand_xoshiro]: https://crates.io/crates/rand_pcg #[cfg_attr(doc_cfg, doc(cfg(feature = "small_rng")))] #[derive(Clone, Debug, PartialEq, Eq)] pub struct SmallRng(Rng); diff --git a/src/rngs/xoshiro128plusplus.rs b/src/rngs/xoshiro128plusplus.rs new file mode 100644 index 00000000000..00f7f6c6dca --- /dev/null +++ b/src/rngs/xoshiro128plusplus.rs @@ -0,0 +1,101 @@ +// Copyright 2018 Developers of the Rand project. +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +#[cfg(feature="serde1")] use serde::{Serialize, Deserialize}; +use rand_core::impls::{next_u64_via_u32, fill_bytes_via_next}; +use rand_core::le::read_u32_into; +use rand_core::{SeedableRng, RngCore, Error}; + +/// A xoshiro128++ random number generator. +/// +/// The xoshiro128++ algorithm is not suitable for cryptographic purposes, but +/// is very fast and has excellent statistical properties. +/// +/// The algorithm used here is translated from [the `xoshiro128plusplus.c` +/// reference source code](http://xoshiro.di.unimi.it/xoshiro128plusplus.c) by +/// David Blackman and Sebastiano Vigna. +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(feature="serde1", derive(Serialize, Deserialize))] +pub struct Xoshiro128PlusPlus { + s: [u32; 4], +} + +impl SeedableRng for Xoshiro128PlusPlus { + type Seed = [u8; 16]; + + /// Create a new `Xoshiro128PlusPlus`. If `seed` is entirely 0, it will be + /// mapped to a different seed. + #[inline] + fn from_seed(seed: [u8; 16]) -> Xoshiro128PlusPlus { + if seed.iter().all(|&x| x == 0) { + return Self::seed_from_u64(0); + } + let mut state = [0; 4]; + read_u32_into(&seed, &mut state); + Xoshiro128PlusPlus { s: state } + } +} + +impl RngCore for Xoshiro128PlusPlus { + #[inline] + fn next_u32(&mut self) -> u32 { + let result_starstar = self.s[0] + .wrapping_add(self.s[3]) + .rotate_left(7) + .wrapping_add(self.s[0]); + + let t = self.s[1] << 9; + + self.s[2] ^= self.s[0]; + self.s[3] ^= self.s[1]; + self.s[1] ^= self.s[2]; + self.s[0] ^= self.s[3]; + + self.s[2] ^= t; + + self.s[3] = self.s[3].rotate_left(11); + + result_starstar + } + + #[inline] + fn next_u64(&mut self) -> u64 { + next_u64_via_u32(self) + } + + #[inline] + fn fill_bytes(&mut self, dest: &mut [u8]) { + fill_bytes_via_next(self, dest); + } + + #[inline] + fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), Error> { + self.fill_bytes(dest); + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn reference() { + let mut rng = Xoshiro128PlusPlus::from_seed( + [1, 0, 0, 0, 2, 0, 0, 0, 3, 0, 0, 0, 4, 0, 0, 0]); + // These values were produced with the reference implementation: + // http://xoshiro.di.unimi.it/xoshiro128plusplus.c + let expected = [ + 641, 1573767, 3222811527, 3517856514, 836907274, 4247214768, + 3867114732, 1355841295, 495546011, 621204420, + ]; + for &e in &expected { + assert_eq!(rng.next_u32(), e); + } + } +} diff --git a/src/rngs/xoshiro256plusplus.rs b/src/rngs/xoshiro256plusplus.rs new file mode 100644 index 00000000000..d8e8295dcc5 --- /dev/null +++ b/src/rngs/xoshiro256plusplus.rs @@ -0,0 +1,103 @@ +// Copyright 2018 Developers of the Rand project. +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +#[cfg(feature="serde1")] use serde::{Serialize, Deserialize}; +use rand_core::impls::fill_bytes_via_next; +use rand_core::le::read_u64_into; +use rand_core::{SeedableRng, RngCore, Error}; + +/// A xoshiro256** random number generator. +/// +/// The xoshiro256** algorithm is not suitable for cryptographic purposes, but +/// is very fast and has excellent statistical properties. +/// +/// The algorithm used here is translated from [the `xoshiro256plusplus.c` +/// reference source code](http://xoshiro.di.unimi.it/xoshiro256plusplus.c) by +/// David Blackman and Sebastiano Vigna. +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(feature="serde1", derive(Serialize, Deserialize))] +pub struct Xoshiro256PlusPlus { + s: [u64; 4], +} + +impl SeedableRng for Xoshiro256PlusPlus { + type Seed = [u8; 32]; + + /// Create a new `Xoshiro256PlusPlus`. If `seed` is entirely 0, it will be + /// mapped to a different seed. + #[inline] + fn from_seed(seed: [u8; 32]) -> Xoshiro256PlusPlus { + if seed.iter().all(|&x| x == 0) { + return Self::seed_from_u64(0); + } + let mut state = [0; 4]; + read_u64_into(&seed, &mut state); + Xoshiro256PlusPlus { s: state } + } +} + +impl RngCore for Xoshiro256PlusPlus { + #[inline] + fn next_u32(&mut self) -> u32 { + self.next_u64() as u32 + } + + #[inline] + fn next_u64(&mut self) -> u64 { + let result_plusplus = self.s[0] + .wrapping_add(self.s[3]) + .rotate_left(23) + .wrapping_add(self.s[0]); + + let t = self.s[1] << 17; + + self.s[2] ^= self.s[0]; + self.s[3] ^= self.s[1]; + self.s[1] ^= self.s[2]; + self.s[0] ^= self.s[3]; + + self.s[2] ^= t; + + self.s[3] = self.s[3].rotate_left(45); + + result_plusplus + } + + #[inline] + fn fill_bytes(&mut self, dest: &mut [u8]) { + fill_bytes_via_next(self, dest); + } + + #[inline] + fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), Error> { + self.fill_bytes(dest); + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn reference() { + let mut rng = Xoshiro256PlusPlus::from_seed( + [1, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, + 3, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0]); + // These values were produced with the reference implementation: + // http://xoshiro.di.unimi.it/xoshiro256plusplus.c + let expected = [ + 41943041, 58720359, 3588806011781223, 3591011842654386, + 9228616714210784205, 9973669472204895162, 14011001112246962877, + 12406186145184390807, 15849039046786891736, 10450023813501588000, + ]; + for &e in &expected { + assert_eq!(rng.next_u64(), e); + } + } +} From dd95631f56972b0f29592f4b9e2a4fbb0a05c30e Mon Sep 17 00:00:00 2001 From: Vinzent Steinberg Date: Sun, 6 Sep 2020 23:57:06 +0200 Subject: [PATCH 2/8] SmallRng: Recommend `rand_chacha` for performance Also fix a wrong link. --- src/rngs/small.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/rngs/small.rs b/src/rngs/small.rs index d9373a34081..6a88442adad 100644 --- a/src/rngs/small.rs +++ b/src/rngs/small.rs @@ -19,8 +19,11 @@ type Rng = super::xoshiro128plusplus::Xoshiro128PlusPlus; /// /// `SmallRng` may be a good choice when a PRNG with small state, cheap /// initialization, good statistical quality and good performance are required. -/// It is **not** a good choice when security against prediction or -/// reproducibility are important. +/// It is **not** a good choice when: +/// - Security against prediction or reproducibility are important. +/// Use [`StdRng`] instead. +/// - The best runtime performance on platforms with SIMD support is required. +/// Use [`StdRng`] or `rand_chacha::ChaCha8Rng` instead. /// /// The algorithm is deterministic but should not be considered reproducible /// due to dependence on platform and possible replacement in future @@ -68,7 +71,7 @@ type Rng = super::xoshiro128plusplus::Xoshiro128PlusPlus; /// [`StdRng`]: crate::rngs::StdRng /// [`thread_rng`]: crate::thread_rng /// [rand_chacha]: https://crates.io/crates/rand_chacha -/// [rand_xoshiro]: https://crates.io/crates/rand_pcg +/// [rand_xoshiro]: https://crates.io/crates/rand_xoshiro #[cfg_attr(doc_cfg, doc(cfg(feature = "small_rng")))] #[derive(Clone, Debug, PartialEq, Eq)] pub struct SmallRng(Rng); From 616ea8794595bdab652829bae1645e0ef91bb46f Mon Sep 17 00:00:00 2001 From: Vinzent Steinberg Date: Mon, 7 Sep 2020 00:02:07 +0200 Subject: [PATCH 3/8] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f634f19e6d3..686cf04b2bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ You may also find the [Upgrade Guide](https://rust-random.github.io/book/update. - Drop some unsafe code (#962, #963, #1011) - Improve treatment of rounding errors in `WeightedIndex::update_weights` (#956) - `StdRng`: Switch from ChaCha20 to ChaCha12 for better performance (#1028) +- `SmallRng`: Replace PCG algorithm with xoshiro{128,256}++ (#1038) ## [0.7.3] - 2020-01-10 ### Fixes From 25ea6c183aeaad7172ab9a6564c9b1be3375b11b Mon Sep 17 00:00:00 2001 From: Vinzent Steinberg Date: Tue, 8 Sep 2020 19:34:24 +0200 Subject: [PATCH 4/8] Revise `SmallRng` advise --- src/rngs/small.rs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/rngs/small.rs b/src/rngs/small.rs index 6a88442adad..0ae6b3fec6e 100644 --- a/src/rngs/small.rs +++ b/src/rngs/small.rs @@ -19,11 +19,16 @@ type Rng = super::xoshiro128plusplus::Xoshiro128PlusPlus; /// /// `SmallRng` may be a good choice when a PRNG with small state, cheap /// initialization, good statistical quality and good performance are required. -/// It is **not** a good choice when: +/// Note that depending on the application, [`StdRng`] is faster on many modern +/// platforms while providing higher-quality randomness. Furthermore, `SmallRng` +/// is **not** a good choice when: /// - Security against prediction or reproducibility are important. /// Use [`StdRng`] instead. -/// - The best runtime performance on platforms with SIMD support is required. -/// Use [`StdRng`] or `rand_chacha::ChaCha8Rng` instead. +/// - Seeds with many zeros are provided. In such cases, it takes `SmallRng` +/// about 10 samples to produce 0 and 1 bits with equal probability. Either +/// provide seeds with an approximately equal number of 0 and 1 (for example +/// by using [`SeedableRng::from_entropy`] or [`SeedableRng::seed_from_u64`]), +/// or use [`StdRng`] instead. /// /// The algorithm is deterministic but should not be considered reproducible /// due to dependence on platform and possible replacement in future @@ -35,7 +40,7 @@ type Rng = super::xoshiro128plusplus::Xoshiro128PlusPlus; /// platform, without consideration for cryptography or security. The size of /// its state is much smaller than [`StdRng`]. The current algorithm is /// `Xoshiro256PlusPlus` on 64-bit platforms and `Xoshiro128PlusPlus` on 32-bit -/// platforms. Both are implemented by the [rand_xoshiro] crate. +/// platforms. Both are also implemented by the [rand_xoshiro] crate. /// /// # Examples /// From 0c8dc1c0c609ea8d7de7aeae210d731861daf8ec Mon Sep 17 00:00:00 2001 From: Vinzent Steinberg Date: Tue, 8 Sep 2020 19:37:36 +0200 Subject: [PATCH 5/8] Fix dead links --- src/rng.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/rng.rs b/src/rng.rs index a5ad2cdf4f8..8162c6f196e 100644 --- a/src/rng.rs +++ b/src/rng.rs @@ -263,7 +263,7 @@ pub trait Rng: RngCore { /// /// If `p < 0` or `p > 1`. /// - /// [`Bernoulli`]: distributions::bernoulli::Bernoulli + /// [`Bernoulli`]: distributions::Bernoulli #[inline] fn gen_bool(&mut self, p: f64) -> bool { let d = distributions::Bernoulli::new(p).unwrap(); @@ -292,7 +292,7 @@ pub trait Rng: RngCore { /// println!("{}", rng.gen_ratio(2, 3)); /// ``` /// - /// [`Bernoulli`]: distributions::bernoulli::Bernoulli + /// [`Bernoulli`]: distributions::Bernoulli #[inline] fn gen_ratio(&mut self, numerator: u32, denominator: u32) -> bool { let d = distributions::Bernoulli::from_ratio(numerator, denominator).unwrap(); From 530cd271248be8629374878a39d394c2ccb7eb25 Mon Sep 17 00:00:00 2001 From: Vinzent Steinberg Date: Thu, 10 Sep 2020 23:53:12 +0200 Subject: [PATCH 6/8] xoshiro256++: Prefer upper bits --- src/rngs/xoshiro256plusplus.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/rngs/xoshiro256plusplus.rs b/src/rngs/xoshiro256plusplus.rs index d8e8295dcc5..be844047eb6 100644 --- a/src/rngs/xoshiro256plusplus.rs +++ b/src/rngs/xoshiro256plusplus.rs @@ -44,7 +44,9 @@ impl SeedableRng for Xoshiro256PlusPlus { impl RngCore for Xoshiro256PlusPlus { #[inline] fn next_u32(&mut self) -> u32 { - self.next_u64() as u32 + // The lowest bits have some linear dependencies, so we use the + // upper bits instead. + (self.next_u64() >> 32) as u32 } #[inline] From 02aafbd99487de7d55a427bd4a98aabe51ce7a6d Mon Sep 17 00:00:00 2001 From: Vinzent Steinberg Date: Mon, 14 Sep 2020 18:50:57 +0200 Subject: [PATCH 7/8] Migrate `SmallRng::seed_from_u64` from PCG32 to SplitMix64 --- src/rngs/xoshiro128plusplus.rs | 17 +++++++++++++++++ src/rngs/xoshiro256plusplus.rs | 17 +++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/src/rngs/xoshiro128plusplus.rs b/src/rngs/xoshiro128plusplus.rs index 00f7f6c6dca..ece98fafd6a 100644 --- a/src/rngs/xoshiro128plusplus.rs +++ b/src/rngs/xoshiro128plusplus.rs @@ -39,6 +39,23 @@ impl SeedableRng for Xoshiro128PlusPlus { read_u32_into(&seed, &mut state); Xoshiro128PlusPlus { s: state } } + + /// Create a new `Xoshiro128PlusPlus` from a `u64` seed. + /// + /// This uses the SplitMix64 generator internally. + fn seed_from_u64(mut state: u64) -> Self { + const PHI: u64 = 0x9e3779b97f4a7c15; + let mut seed = Self::Seed::default(); + for chunk in seed.as_mut().chunks_mut(8) { + state = state.wrapping_add(PHI); + let mut z = state; + z = (z ^ (z >> 30)).wrapping_mul(0xbf58476d1ce4e5b9); + z = (z ^ (z >> 27)).wrapping_mul(0x94d049bb133111eb); + z = z ^ (z >> 31); + chunk.copy_from_slice(&z.to_le_bytes()); + } + Self::from_seed(seed) + } } impl RngCore for Xoshiro128PlusPlus { diff --git a/src/rngs/xoshiro256plusplus.rs b/src/rngs/xoshiro256plusplus.rs index be844047eb6..cd373c30669 100644 --- a/src/rngs/xoshiro256plusplus.rs +++ b/src/rngs/xoshiro256plusplus.rs @@ -39,6 +39,23 @@ impl SeedableRng for Xoshiro256PlusPlus { read_u64_into(&seed, &mut state); Xoshiro256PlusPlus { s: state } } + + /// Create a new `Xoshiro256PlusPlus` from a `u64` seed. + /// + /// This uses the SplitMix64 generator internally. + fn seed_from_u64(mut state: u64) -> Self { + const PHI: u64 = 0x9e3779b97f4a7c15; + let mut seed = Self::Seed::default(); + for chunk in seed.as_mut().chunks_mut(8) { + state = state.wrapping_add(PHI); + let mut z = state; + z = (z ^ (z >> 30)).wrapping_mul(0xbf58476d1ce4e5b9); + z = (z ^ (z >> 27)).wrapping_mul(0x94d049bb133111eb); + z = z ^ (z >> 31); + chunk.copy_from_slice(&z.to_le_bytes()); + } + Self::from_seed(seed) + } } impl RngCore for Xoshiro256PlusPlus { From a260f2cee20781cc15beac2a0adeb9f9521cd9db Mon Sep 17 00:00:00 2001 From: Vinzent Steinberg Date: Mon, 14 Sep 2020 19:37:21 +0200 Subject: [PATCH 8/8] Improve `SmallRng` advise --- src/rngs/small.rs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/rngs/small.rs b/src/rngs/small.rs index 0ae6b3fec6e..fb0e0d119b6 100644 --- a/src/rngs/small.rs +++ b/src/rngs/small.rs @@ -19,11 +19,10 @@ type Rng = super::xoshiro128plusplus::Xoshiro128PlusPlus; /// /// `SmallRng` may be a good choice when a PRNG with small state, cheap /// initialization, good statistical quality and good performance are required. -/// Note that depending on the application, [`StdRng`] is faster on many modern -/// platforms while providing higher-quality randomness. Furthermore, `SmallRng` -/// is **not** a good choice when: -/// - Security against prediction or reproducibility are important. -/// Use [`StdRng`] instead. +/// Note that depending on the application, [`StdRng`] may be faster on many +/// modern platforms while providing higher-quality randomness. Furthermore, +/// `SmallRng` is **not** a good choice when: +/// - Security against prediction is important. Use [`StdRng`] instead. /// - Seeds with many zeros are provided. In such cases, it takes `SmallRng` /// about 10 samples to produce 0 and 1 bits with equal probability. Either /// provide seeds with an approximately equal number of 0 and 1 (for example