Skip to main content

rand_float/
campbell.rs

1// Derived from binary64fast.c and random_real.c by Taylor R. Campbell,
2// distributed under the following license:
3//
4// Copyright (c) 2014-2026 Taylor R. Campbell
5// All rights reserved.
6//
7// Redistribution and use in source and binary forms, with or without
8// modification, are permitted provided that the following conditions
9// are met:
10// 1. Redistributions of source code must retain the above copyright
11//    notice, this list of conditions and the following disclaimer.
12// 2. Redistributions in binary form must reproduce the above copyright
13//    notice, this list of conditions and the following disclaimer in the
14//    documentation and/or other materials provided with the distribution.
15//
16// THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
17// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
18// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
19// ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
20// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
21// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
22// OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
23// HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
24// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
25// OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
26// SUCH DAMAGE.
27
28//! Taylor R. Campbell’s correctly rounded uniform doubles.
29//!
30//! A Rust port of `binary64fast.c` (Campbell, 2014–2026) and of the
31//! `random_real` function of [`random_real.c`] (Campbell, 2014). These
32//! functions return an `f64` distributed as a uniform
33//! real in [0 . . 1] correctly rounded to nearest. The `binary64fast.c`
34//! functions ([`fast`] and the const-time variants) stop after two exponent
35//! words, so they reach every float in [2⁻¹²⁸ . . 1], each with
36//! probability equal to the measure of the reals that round to it, except
37//! that the reals below 2⁻¹²⁸ (probability 2⁻¹²⁸) are folded into the bottom
38//! binade. In particular, 0 never occurs: [`real`] keeps consuming words,
39//! and reaches every float in [2⁻¹⁰²⁴ . . 1], plus 0 (see its documentation
40//! for why the smaller subnormals cannot occur).
41//!
42//! Two variants handle the zero-`m` event without data-dependent branching, for
43//! use where timing side channels matter; they unconditionally consume three
44//! words per call. The `cmov` variant is a modification of mine in which the
45//! “bit smearing“ technique of the original C is replaced by a comparison that
46//! the compiler can turn into a conditional-move instruction (Taylor does not
47//! want to rely on the compiler for the absence of tests, but the smearing is
48//! much slower.)
49//!
50//! If you compare generated code with older copies of `binary64fast.c` you
51//! might find a signed where an unsigned integer-to-double conversion
52//! instruction was present: Taylor fixed the code after I reported the
53//! possibility of a branchy unsigned conversion on pre-AVX-512 x86.
54//!
55//! [`random_real.c`]: https://mumble.net/~campbell/2014/04/28/random_real.c
56
57/// 2⁻⁶⁴ (Rust has no hex float literals; built via the exponent field).
58const TWO_M64: f64 = f64::from_bits((1023 - 64) << 52);
59/// 2³², used by the x86-only split unsigned→double conversion.
60#[cfg(target_arch = "x86_64")]
61const TWO_P32: f64 = 4294967296.0;
62
63/// Port of Campbell’s `uniformbinary64_fastdet`, the deterministic core of
64/// this module: turns an exponent scale `f` ∈ {2⁻⁶⁴, 2⁻¹²⁸}, a geometric
65/// word `m` and a significand word `u` into a correctly rounded (to
66/// nearest) uniform binary64.
67#[inline(always)]
68pub const fn fastdet(f: f64, m: u64, u: u64) -> f64 {
69    // Largest power-of-two divisor of m, with bit 63 forced as a backstop
70    // against a broken all-zero source; exactly representable, so the
71    // conversion below is exact.
72    let m = m | (1 << 63);
73    let m = m & m.wrapping_neg();
74    // On x86_64 there is no unsigned 64-bit → double instruction until
75    // AVX-512; split into halves so the compiler emits branch-free signed
76    // conversions, as in the C original.
77    #[cfg(target_arch = "x86_64")]
78    let d = ((m >> 32) as f64) * TWO_P32 + ((m & 0xFFFF_FFFF) as f64);
79    #[cfg(not(target_arch = "x86_64"))]
80    let d = m as f64;
81
82    // Uniform odd integer in (2⁶³..2⁶⁴): round-to-odd of a uniform real in
83    // [2⁶³..2⁶⁴]. The conversion rounds to nearest; ties are impossible.
84    let u = u | (1 << 63) | 1;
85    let s = u as f64;
86
87    // Scale the significand into [1/2..1] and apply the geometric exponent.
88    s * f / d
89}
90
91/// Port of `uniformbinary64_fast`: a correctly rounded (to nearest) uniform
92/// real in [0 . . 1], branching on the 2⁻⁶⁴-probability zero-`m` event.
93///
94/// Consumes two 64-bit words, plus a third with probability 2⁻⁶⁴.
95#[inline(always)]
96pub fn fast(mut bits: impl FnMut() -> u64) -> f64 {
97    let u = bits();
98    let mut f = TWO_M64;
99    let mut m = bits();
100    if m == 0 {
101        // unlikely
102        f *= TWO_M64;
103        m = bits();
104    }
105    fastdet(f, m, u)
106}
107
108/// Shared tail of the const-time variants: given the flag t ∈ {0, 1}
109/// (t = 1 iff m ≠ 0), rescale `f` and substitute `m2` for a zero `m`,
110/// both branch-free. The rescaling is in signed arithmetic, fixing a bug
111/// of the original C.
112#[inline(always)]
113const fn consttime_tail(t: u64, u: u64, m: u64, m2: u64) -> f64 {
114    let mut f = TWO_M64;
115    let tf = t as i64 as f64;
116    f *= tf - (tf - 1.0) * TWO_M64;
117    let m = m | (m2 & t.wrapping_sub(1));
118    fastdet(f, m, u)
119}
120
121/// Modified port of `uniformbinary64_consttime`: like [`fast`], but the zero-`m`
122/// event is branchless, with the flag computed as `m != 0`.
123///
124/// This is a small variant of mine: it relies on the compiler turning the
125/// comparison into a conditional-move–style instruction (`setne`, `cset`)
126/// rather than a branch. Taylor prefers the bit-smearing variant
127/// ([`consttime`]) because, for security reasons, it guarantees at the source
128/// level that no test can be inserted by the compiler, whereas here the absence
129/// of a branch is at the optimizer's discretion.
130///
131/// Always consumes three 64-bit words.
132#[inline(always)]
133pub fn consttime_cmove(mut bits: impl FnMut() -> u64) -> f64 {
134    let (u, m, m2) = (bits(), bits(), bits());
135    let t = (m != 0) as u64;
136    consttime_tail(t, u, m, m2)
137}
138
139/// Port of `uniformbinary64_consttime_smear`: like [`consttime_cmove`], but the
140/// flag is computed by smearing every set bit of `m` down to bit 0, so it is
141/// branchless at the source level, independently of the compiler. This is the
142/// variant Taylor prefers for security-sensitive uses, since no
143/// compiler-inserted test can leak the (secret) value of `m` through a timing
144/// side channel.
145///
146/// Always consumes three 64-bit words.
147#[inline(always)]
148pub fn consttime(mut bits: impl FnMut() -> u64) -> f64 {
149    let (u, m, m2) = (bits(), bits(), bits());
150    let mut t = m;
151    t |= t >> 1;
152    t |= t >> 2;
153    t |= t >> 4;
154    t |= t >> 8;
155    t |= t >> 16;
156    t |= t >> 32;
157    t &= 1;
158    consttime_tail(t, u, m, m2)
159}
160
161/// 2⁻⁹⁶⁰, the exact first stage of the two-step scaling that replaces
162/// the C original's `ldexp`.
163#[cfg(not(all(target_arch = "x86_64", target_feature = "avx512f")))]
164const TWO_M960: f64 = f64::from_bits((1023 - 960) << 52);
165
166/// `ldexp(significand as f64, exponent)` for the tail of [`real`]:
167/// AVX-512F provides `ldexp` in hardware (`vscalefsd`, x·2^⌊e⌋ with a
168/// single rounding, subnormals included), so a single instruction
169/// replaces the two-multiplication sequence of the portable version
170/// below.
171#[cfg(all(target_arch = "x86_64", target_feature = "avx512f"))]
172#[inline(always)]
173fn ldexp_sig(significand: u64, exponent: i32) -> f64 {
174    let x = significand as f64; // vcvtusi2sd: single instruction on AVX-512
175    let e = exponent as f64; // exact, and off the critical path
176    let r: f64;
177    // SAFETY: pure register-to-register scalar arithmetic; the cfg above
178    // guarantees the instruction set.
179    unsafe {
180        core::arch::asm!(
181            "vscalefsd {r}, {x}, {e}",
182            r = lateout(xmm_reg) r,
183            x = in(xmm_reg) x,
184            e = in(xmm_reg) e,
185            options(pure, nomem, nostack),
186        );
187    }
188    r
189}
190
191/// `ldexp(significand as f64, exponent)` in two multiplications: the
192/// first is exact, the second rounds (to a subnormal, possibly).
193#[cfg(not(all(target_arch = "x86_64", target_feature = "avx512f")))]
194#[inline(always)]
195fn ldexp_sig(significand: u64, exponent: i32) -> f64 {
196    significand as f64 * TWO_M960 * f64::from_bits(((1023 + 960 + exponent) as u64) << 52)
197}
198
199/// Port of `random_real` from `random_real.c`: interprets an unbounded
200/// stream of random bits as the binary expansion of a real number in
201/// [0 . . 1] and rounds it to the nearest `f64`, with a sticky bit avoiding
202/// ties. Every float in [2⁻¹⁰²⁴ . . 1] is reachable; 0 is returned after
203/// 1024 zero bits (probability 2⁻¹⁰²⁴, i.e., only if the source is broken),
204/// and the subnormals below 2⁻¹⁰²⁴ cannot occur.
205///
206/// This documentation differs from the comments in the C original, which
207/// claims to reach every float in [0 . . 1] and to return 0 only when the
208/// result is guaranteed to round to zero: the all-zeros cutoff fires one
209/// word too early (after 16 rather than 17 zero words) discarding
210/// continuations as large as ≈2⁻¹⁰²⁴ that would round to smaller
211/// subnormals. This is a minor bug we found while porting and reported to
212/// the author; it affects an event of probability 2⁻¹⁰²⁴, and the code
213/// below is left faithful to the original.
214///
215/// Consumes one 64-bit word, plus one more with probability 1/2 (when the
216/// first word has leading zeros), plus one word per 64 leading zero bits.
217/// The expected number of words per call is ≈1.5.
218///
219/// The C original scales by `ldexp(significand, exponent)`; Rust has no
220/// `ldexp` in the standard library, and a single multiplication cannot
221/// replace it, since 2^exponent can be as small as 2⁻¹⁰⁸⁷, which is not
222/// representable. The port multiplies by 2⁻⁹⁶⁰ first (exact, since the
223/// intermediate result stays normal) and then by 2^(exponent + 960),
224/// which is always representable, so the result is rounded once, exactly
225/// as in `ldexp`. On x86-64 compiled with AVX-512F the port uses instead
226/// the hardware `ldexp` (a single `vscalefsd` instruction) via inline
227/// assembly.
228#[inline(always)]
229pub fn real(mut bits: impl FnMut() -> u64) -> f64 {
230    let mut exponent = -64i32;
231
232    // Read zeros into the exponent until we hit a one; the rest will go
233    // into the significand.
234    let mut significand = bits();
235    while significand == 0 {
236        exponent -= 64;
237        // The C original claims that below -1074 = emin + 1 - p the result
238        // is guaranteed to round to zero, but the check fires one word too
239        // early (see the doc comment); kept as is for faithfulness to the
240        // original. This can happen in realistic terms only if the source
241        // is broken.
242        if exponent < -1074 {
243            return 0.0;
244        }
245        significand = bits();
246    }
247
248    // If there are leading zeros, shift them into the exponent and refill
249    // the less-significant bits of the significand.
250    let shift = significand.leading_zeros();
251    if shift != 0 {
252        exponent -= shift as i32;
253        significand <<= shift;
254        significand |= bits() >> (64 - shift);
255    }
256
257    // Set the sticky bit, since there is almost surely another 1 in the
258    // bit stream; otherwise an apparent tie might round to even when,
259    // almost surely, a further 1 would break it.
260    significand |= 1;
261
262    ldexp_sig(significand, exponent)
263}
264
265#[cfg(test)]
266mod tests {
267    use super::*;
268    use crate::sources::Weyl;
269
270    /// 2⁻¹²⁸.
271    const TWO_M128: f64 = f64::from_bits((1023 - 128) << 52);
272
273    /// A source that replays a fixed sequence of words, then panics.
274    fn replay(words: &[u64]) -> impl FnMut() -> u64 + '_ {
275        let mut iter = words.iter();
276        move || *iter.next().expect("source exhausted")
277    }
278
279    #[test]
280    fn test_stays_in_closed_unit_interval() {
281        let mut rng = Weyl(42);
282        for _ in 0..100_000 {
283            let x = fast(|| rng.next_u64());
284            assert!(x > 0.0 && x <= 1.0, "fast: {x}");
285            let x = consttime_cmove(|| rng.next_u64());
286            assert!(x > 0.0 && x <= 1.0, "consttime_cmove: {x}");
287            let x = consttime(|| rng.next_u64());
288            assert!(x > 0.0 && x <= 1.0, "consttime: {x}");
289        }
290    }
291
292    /// The two const-time variants must agree on every input.
293    #[test]
294    fn test_consttime_variants_agree() {
295        let mut rng = Weyl(0xDEAD_BEEF);
296        for _ in 0..100_000 {
297            let words = [rng.next_u64(), rng.next_u64(), rng.next_u64()];
298            assert_eq!(consttime_cmove(replay(&words)), consttime(replay(&words)));
299        }
300    }
301
302    /// With a nonzero m, the const-time variants must agree with the
303    /// branchy variant (which then ignores the third word).
304    #[test]
305    fn test_consttime_agrees_with_fast() {
306        let mut rng = Weyl(0xBADC_0FFE);
307        for _ in 0..100_000 {
308            let words = [rng.next_u64(), rng.next_u64().max(1), rng.next_u64()];
309            assert_eq!(fast(replay(&words[..2])), consttime_cmove(replay(&words)));
310        }
311    }
312
313    /// The signed-arithmetic fix: with m = 0 the scale must become 2⁻¹²⁸
314    /// (not go negative as with the unsigned `(t - 1)` of the original C).
315    #[test]
316    fn test_consttime_zero_mantissa_rescales() {
317        let u = 0x0123_4567_89AB_CDEF;
318        let m2 = 0x8000_0000_0000_0000u64; // power of two: d = 2^63
319        let x = consttime_cmove(replay(&[u, 0, m2]));
320        assert!(x > 0.0, "scale went negative: {x}");
321        // s ≈ 2^63, f = 2^-128, d = 2^63  ⇒  x ≈ 2^-128.
322        assert_eq!(x, fastdet(TWO_M128, m2, u));
323        // And with m ≠ 0, m2 must be ignored.
324        assert_eq!(consttime_cmove(replay(&[u, 3, m2])), fastdet(TWO_M64, 3, u));
325    }
326
327    /// Known values of `real`, pinning the two-step `ldexp` replacement
328    /// (verified bit-for-bit against the compiled C original).
329    #[test]
330    fn test_real_known_values() {
331        // Top bit set: one word, no refill; 2^63 | 1 rounds to 2^63.
332        assert_eq!(real(replay(&[1 << 63])), 0.5);
333        // All ones rounds up to 2^64: the maximum output, exactly 1.
334        assert_eq!(real(replay(&[!0])), 1.0);
335        // Smallest first word: 63 leading zeros shifted out and refilled.
336        assert_eq!(real(replay(&[1, 0])), f64::from_bits((1023 - 64) << 52)); // 2^-64
337        // 15 zero words then a 1: deep subnormal, 2^-1024.
338        let mut words = [0u64; 17];
339        words[15] = 1;
340        assert_eq!(real(replay(&words)), f64::from_bits(1 << 50));
341        // 16 zero words: the exponent falls below -1074 and the result is 0.
342        assert_eq!(real(replay(&[0u64; 16])), 0.0);
343    }
344
345    #[test]
346    fn test_real_stays_in_closed_unit_interval() {
347        let mut rng = Weyl(7);
348        for _ in 0..100_000 {
349            let x = real(|| rng.next_u64());
350            assert!(x > 0.0 && x <= 1.0, "real: {x}");
351        }
352    }
353}