Skip to content

Commit 6ceb4f1

Browse files
authored
Improve quality of f32/f64 generation. (#103)
The previous int-to-float conversion had a bias of probability 2^-24 / 2^-53 for types f32 / f64 respectively. The new conversion has a bias of 2^-64, which is the same bias as the underlying WyRand generator. The new conversion is a slightly shorter instruction sequence on x86 and ARM, but executes as 1 more uop on x86. Seems unlikely to harm performance much, if at all. https://rust.godbolt.org/z/q3zMxEc3T
1 parent 9c536a4 commit 6ceb4f1

File tree

3 files changed

+118
-46
lines changed

3 files changed

+118
-46
lines changed

src/global_rng.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,11 +176,21 @@ pub fn f32() -> f32 {
176176
with_rng(|r| r.f32())
177177
}
178178

179+
/// Generates a random `f32` in range `0..=1`.
180+
pub fn f32_inclusive() -> f32 {
181+
with_rng(|r| r.f32_inclusive())
182+
}
183+
179184
/// Generates a random `f64` in range `0..1`.
180185
pub fn f64() -> f64 {
181186
with_rng(|r| r.f64())
182187
}
183188

189+
/// Generates a random `f64` in range `0..=1`.
190+
pub fn f64_inclusive() -> f64 {
191+
with_rng(|r| r.f64_inclusive())
192+
}
193+
184194
/// Collects `amount` values at random from the iterable into a vector.
185195
pub fn choose_multiple<I: IntoIterator>(source: I, amount: usize) -> Vec<I::Item> {
186196
with_rng(|rng| rng.choose_multiple(source, amount))

src/lib.rs

Lines changed: 55 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -364,18 +364,67 @@ impl Rng {
364364
}
365365
}
366366

367+
/// Generates a random `f32` in range `0..=1`.
368+
#[inline]
369+
pub fn f32_inclusive(&mut self) -> f32 {
370+
// Generate a number in 0..2^63 then convert to f32 and multiply by 2^(-63).
371+
//
372+
// Even though we're returning f32, we still generate u64 internally to make
373+
// it possible to return nonzero numbers as small as 2^(-63). If we only
374+
// generated u32 internally, the smallest nonzero number we could return
375+
// would be 2^(-32).
376+
//
377+
// The integer we generate is in 0..2^63 rather than 0..2^64 to improve speed
378+
// on x86-64, which has efficient i64->float conversion (cvtsi2ss) but for
379+
// which u64->float conversion must be implemented in software.
380+
//
381+
// There is still some remaining bias in the int-to-float conversion, because
382+
// nonzero numbers <=2^(-64) are never generated, even though they are
383+
// expressible in f32. However, at this point the bias in int-to-float conversion
384+
// is no larger than the bias in the underlying WyRand generator: since it only
385+
// has a 64-bit state, it necessarily already have biases of at least 2^(-64)
386+
// probability.
387+
//
388+
// See e.g. Section 3.1 of Thomas, David B., et al. "Gaussian random number generators,
389+
// https://www.doc.ic.ac.uk/~wl/papers/07/csur07dt.pdf, for background.
390+
const MUL: f32 = 1.0 / (1u64 << 63) as f32;
391+
(self.gen_u64() >> 1) as f32 * MUL
392+
}
393+
367394
/// Generates a random `f32` in range `0..1`.
395+
///
396+
/// Function `f32_inclusive()` is a little simpler and faster, so default
397+
/// to that if inclusive range is acceptable.
398+
#[inline]
368399
pub fn f32(&mut self) -> f32 {
369-
let b = 32;
370-
let f = core::f32::MANTISSA_DIGITS - 1;
371-
f32::from_bits((1 << (b - 2)) - (1 << f) + (self.u32(..) >> (b - f))) - 1.0
400+
loop {
401+
let x = self.f32_inclusive();
402+
if x < 1.0 {
403+
return x;
404+
}
405+
}
406+
}
407+
408+
/// Generates a random `f64` in range `0..=1`.
409+
#[inline]
410+
pub fn f64_inclusive(&mut self) -> f64 {
411+
// See the comment in f32_inclusive() for more details.
412+
const MUL: f64 = 1.0 / (1u64 << 63) as f64;
413+
(self.gen_u64() >> 1) as f64 * MUL
372414
}
373415

374416
/// Generates a random `f64` in range `0..1`.
417+
///
418+
/// Function `f64_inclusive()` is a little simpler and faster, so default
419+
/// to that if inclusive range is acceptable.
420+
#[inline]
375421
pub fn f64(&mut self) -> f64 {
376-
let b = 64;
377-
let f = core::f64::MANTISSA_DIGITS - 1;
378-
f64::from_bits((1 << (b - 2)) - (1 << f) + (self.u64(..) >> (b - f))) - 1.0
422+
loop {
423+
let x = self.f64_inclusive();
424+
if x < 1.0 {
425+
return x;
426+
}
427+
}
379428
}
380429

381430
/// Collects `amount` values at random from the iterable into a vector.

tests/smoke.rs

Lines changed: 53 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -77,59 +77,72 @@ fn u128() {
7777

7878
#[test]
7979
fn f32() {
80-
for _ in 0..1000 {
81-
let result = fastrand::f32();
82-
assert!((0.0..1.0).contains(&result));
83-
}
84-
}
85-
86-
#[test]
87-
fn f64() {
88-
for _ in 0..1000 {
89-
let result = fastrand::f64();
90-
assert!((0.0..1.0).contains(&result));
91-
}
92-
}
93-
94-
#[test]
95-
fn digit() {
96-
for base in 1..36 {
97-
let result = fastrand::digit(base);
98-
assert!(result.is_ascii_digit() || result.is_ascii_lowercase());
99-
}
100-
}
101-
102-
#[test]
103-
fn global_rng_choice() {
104-
let items = [1, 4, 9, 5, 2, 3, 6, 7, 8, 0];
105-
106-
for item in &items {
107-
while fastrand::choice(&items).unwrap() != item {}
80+
let mut r = fastrand::Rng::with_seed(0);
81+
let tiny = (-24.0f32).exp2();
82+
let mut count_tiny_nonzero = 0;
83+
let mut count_top_half = 0;
84+
for _ in 0..100_000_000 {
85+
let x = r.f32();
86+
assert!((0.0..1.0).contains(&x));
87+
if x > 0.0 && x < tiny {
88+
count_tiny_nonzero += 1;
89+
} else if x > 0.5 {
90+
count_top_half += 1;
91+
}
10892
}
93+
assert!(count_top_half >= 49_000_000);
94+
assert!(count_tiny_nonzero > 0);
10995
}
11096

11197
#[test]
112-
fn global_rng_alphabetic() {
113-
for _ in 0..1000 {
114-
let result = fastrand::alphabetic();
115-
assert!(result.is_ascii_alphabetic())
98+
fn f32_inclusive() {
99+
let mut r = fastrand::Rng::with_seed(0);
100+
let tiny = (-24.0f32).exp2();
101+
let mut count_top_half = 0;
102+
let mut count_tiny_nonzero = 0;
103+
let mut count_one = 0;
104+
for _ in 0..100_000_000 {
105+
let x = r.f32_inclusive();
106+
assert!((0.0..=1.0).contains(&x));
107+
if x == 1.0 {
108+
count_one += 1;
109+
} else if x > 0.5 {
110+
count_top_half += 1;
111+
} else if x > 0.0 && x < tiny {
112+
count_tiny_nonzero += 1;
113+
}
116114
}
115+
assert!(count_top_half >= 49_000_000);
116+
assert!(count_one > 0);
117+
assert!(count_tiny_nonzero > 0);
117118
}
118119

119120
#[test]
120-
fn global_rng_lowercase() {
121-
for _ in 0..1000 {
122-
let result = fastrand::lowercase();
123-
assert!(result.is_ascii_lowercase())
121+
fn f64() {
122+
let mut r = fastrand::Rng::with_seed(0);
123+
let mut count_top_half = 0;
124+
for _ in 0..100_000_000 {
125+
let x = r.f64();
126+
assert!((0.0..1.0).contains(&x));
127+
if x > 0.5 {
128+
count_top_half += 1;
129+
}
124130
}
131+
assert!(count_top_half >= 49_000_000);
125132
}
126133

127134
#[test]
128-
fn global_rng_uppercase() {
129-
for _ in 0..1000 {
130-
let result = fastrand::uppercase();
131-
assert!(result.is_ascii_uppercase())
135+
fn f64_inclusive() {
136+
let mut r = fastrand::Rng::with_seed(0);
137+
let mut count_top_half = 0;
138+
for _ in 0..100_000_000 {
139+
let x = r.f64_inclusive();
140+
assert!((0.0..=1.0).contains(&x));
141+
if x > 0.5 {
142+
count_top_half += 1;
143+
}
132144
}
145+
assert!(count_top_half >= 49_000_000);
133146
}
134147

135148
#[test]

0 commit comments

Comments
 (0)