Skip to content

Commit 44476da

Browse files
committed
osi/rng: implement Xoshiro256++
Implement the Xoshiro256++ general-purpose PRNG, using SplitMix64 as optional seeding. Signed-off-by: David Rheinsberg <david@readahead.eu>
1 parent df0c1e9 commit 44476da

1 file changed

Lines changed: 177 additions & 0 deletions

File tree

lib/osi/src/rng.rs

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,37 @@ pub struct SplitMix64 {
4141
gamma: u64,
4242
}
4343

44+
/// Random Number Generator using Xoshiro256++.
45+
///
46+
/// This is the state of the Xoshiro256++ pseudo random number generator. It
47+
/// uses 256-bit of state and generates 64-bit random numbers. This PRNG is
48+
/// **not** cryptgraphically secure, but is otherwise a good fit for nearly
49+
/// all purposes.
50+
///
51+
/// This implements `Clone` and `Copy` for verbatim copies. Use
52+
/// [`jump128()`](Self::jump128) to produce non-verbatim copies with better
53+
/// random distribution.
54+
#[derive(Clone, Debug, Eq, PartialEq)]
55+
pub struct Xoshiro256pp {
56+
state: [u64; 4],
57+
}
58+
59+
// Static calculation of the first few values from Mix64(0). The array is
60+
// statically verified to contain no 0 values (which sufficiently resolves
61+
// non-0 seeding requirements of some RNGs).
62+
const MIX64_0: [u64; 4] = {
63+
let mut mix = Mix64::with_seed(0);
64+
let v = [mix.next64(); 4];
65+
66+
let mut i = 0;
67+
while i < v.len() {
68+
assert!(v[i] != 0);
69+
i += 1;
70+
}
71+
72+
v
73+
};
74+
4475
impl Mix64 {
4576
const GAMMA: u64 = 0x9e3779b97f4a7c15;
4677

@@ -168,6 +199,125 @@ impl From<Mix64> for SplitMix64 {
168199
}
169200
}
170201

202+
impl Xoshiro256pp {
203+
const fn combine(s: &[u64; 4]) -> u64 {
204+
s[0].wrapping_add(s[3])
205+
.rotate_left(23)
206+
.wrapping_add(s[0])
207+
}
208+
209+
const fn step(s: &mut [u64; 4]) {
210+
let t = s[1] << 17;
211+
212+
s[2] ^= s[0];
213+
s[3] ^= s[1];
214+
s[1] ^= s[2];
215+
s[0] ^= s[3];
216+
217+
s[2] ^= t;
218+
s[3] = s[3].rotate_left(45);
219+
}
220+
221+
const fn jump(s: &mut [u64; 4], jump_table: &[u64; 4]) {
222+
let mut t = [0; 4];
223+
224+
let mut i = 0;
225+
while i < 4 {
226+
let mut j = 0;
227+
while j < 64 {
228+
if (jump_table[i] & (1 << j)) != 0 {
229+
t[0] ^= s[0];
230+
t[1] ^= s[1];
231+
t[2] ^= s[2];
232+
t[3] ^= s[3];
233+
}
234+
Self::step(s);
235+
j += 1;
236+
}
237+
i += 1;
238+
}
239+
240+
*s = t;
241+
}
242+
243+
// Steps 2^128 times, using pre-calculated jump tables.
244+
const fn step128(s: &mut [u64; 4]) {
245+
Self::jump(
246+
s,
247+
&[
248+
0x180ec6d33cfd0aba,
249+
0xd5a61266f0c9392c,
250+
0xa9582618e03fc9aa,
251+
0x39abdc4529b1661c,
252+
],
253+
)
254+
}
255+
256+
// Steps 2^192 times, using pre-calculated jump tables.
257+
const fn step192(s: &mut [u64; 4]) {
258+
Self::jump(
259+
s,
260+
&[
261+
0x76e15d3efefdcbbf,
262+
0xc5004e441c522fb3,
263+
0x77710069854ee241,
264+
0x39109bb02acbe635,
265+
],
266+
)
267+
}
268+
269+
/// Create a new instance with the given seed.
270+
///
271+
/// The seed is used unmodified as the internal state of the Xoshiro256++
272+
/// RNG, and thus will produce the same results as other implementations
273+
/// with this seed (except if the seed is 0, described below).
274+
///
275+
/// A seed of all 0 is not allowed for Xoshiro256++. This implementation
276+
/// maps a seed of all 0 to `Self::from_splitmix64(0)`.
277+
pub const fn with_seed(seed: [u64; 4]) -> Self {
278+
if seed[0] == 0 && seed[1] == 0 && seed[2] == 0 && seed[3] == 0 {
279+
Self {
280+
state: MIX64_0,
281+
}
282+
} else {
283+
Self {
284+
state: seed,
285+
}
286+
}
287+
}
288+
289+
/// Create a new instance from a SplitMix64.
290+
///
291+
/// Use the [`SplitMix64`] instance to seed the 256-bit of state of a new
292+
/// RNG instance.
293+
pub const fn from_splitmix64(mix: &mut SplitMix64) -> Self {
294+
Self::with_seed([mix.next64(); 4])
295+
}
296+
297+
/// Produce the next 64-bit random number.
298+
pub const fn next64(&mut self) -> u64 {
299+
let r = Self::combine(&self.state);
300+
Self::step(&mut self.state);
301+
r
302+
}
303+
304+
/// Jump over the next 2^128 random numbers.
305+
pub const fn jump128(&mut self) {
306+
Self::step128(&mut self.state);
307+
}
308+
309+
/// Jump over the next 2^192 random numbers.
310+
pub const fn jump192(&mut self) {
311+
Self::step192(&mut self.state);
312+
}
313+
}
314+
315+
impl From<SplitMix64> for Xoshiro256pp {
316+
fn from(mut v: SplitMix64) -> Self {
317+
Self::from_splitmix64(&mut v)
318+
}
319+
}
320+
171321
#[cfg(test)]
172322
mod test {
173323
use super::*;
@@ -229,4 +379,31 @@ mod test {
229379
assert_eq!(rng0.split(), rng1.split());
230380
}
231381
}
382+
383+
// Run some known-value-tests on Xoshiro256pp.
384+
#[test]
385+
fn xoshiro256pp_known() {
386+
{
387+
let mut rng0 = Xoshiro256pp::with_seed([1, 2, 3, 4]);
388+
389+
assert_eq!(rng0.next64(), 41943041);
390+
assert_eq!(rng0.next64(), 58720359);
391+
assert_eq!(rng0.next64(), 3588806011781223);
392+
assert_eq!(rng0.next64(), 3591011842654386);
393+
394+
rng0.jump128();
395+
396+
assert_eq!(rng0.next64(), 10838999831620499216);
397+
assert_eq!(rng0.next64(), 8680420094678800874);
398+
assert_eq!(rng0.next64(), 9570055643283944810);
399+
assert_eq!(rng0.next64(), 7079802948504130534);
400+
401+
rng0.jump192();
402+
403+
assert_eq!(rng0.next64(), 7229965972965062926);
404+
assert_eq!(rng0.next64(), 2140690761664815708);
405+
assert_eq!(rng0.next64(), 5733913562642225265);
406+
assert_eq!(rng0.next64(), 10699737370828579003);
407+
}
408+
}
232409
}

0 commit comments

Comments
 (0)