Why do Zig, Rust and Julia give different random numbers when I give them the same seed and algorithm?

I have this sample code snippet in Zig that generates 10 random f32 and puts them in a slice.

const std = @import("std");

pub fn main() void {
    var prng_state = std.Random.Xoshiro256.init(0);
    const prng = prng_state.random();
    var slice: [10]f32 = undefined;
    
    for (0..slice.len) |idx| {
        slice[idx] = prng.float(f32);
    }
    
    std.debug.print("{any}\n", .{slice});
}

This outputs the following:

{ 0.27175805, 0.25162527, 0.30172718, 0.011619115, 0.3952554, 0.029364396, 0.7070683, 0.7960411, 0.26941678, 0.094871685 }

Same snippet in Rust

use rand::prelude::*;
use rand_distr::Uniform;
use std::error::Error;

fn main() -> Result<(), Box<dyn Error>> {
    let rng = rand::rngs::Xoshiro256PlusPlus::from_seed([0u8; 32]);
    let arr: Vec<f32> = rng.sample_iter(Uniform::new(0.0, 1.0)?).take(10).collect();

    eprintln!("{arr:?}");
    Ok(())
}

Outputs this:

[0.3245752, 0.38223922, 0.3596171, 0.011455417, 0.49527, 0.020565152, 0.85724735, 0.8455087, 0.29488564, 0.07423377]

I checked to make sure that std.Random.Xoshiro256 is actually the Xoshiro256++ algorithm and the comment on the file states it is.

//! Xoshiro256++ - http://xoroshiro.di.unimi.it/
//!
//! PRNG

Just to double check, I used Julia to generate 10 random numbers too, and it seemed to give different numbers too, which makes me think it’s something I’m doing incorrectly.

julia> using Random

julia> rand(Xoshiro(0), Float32, 10)
10-element Vector{Float32}:
 0.48576927
 0.40569943
 0.017441511
 0.06854582
 0.2000143
 0.86214083
 0.27072555
 0.08597082
 0.30745035
 0.6616127

Any ideas what’s up? I wanted to get the sequence of random numbers to write tests for a function that depends on a prng.

Read the source code? It’s possible that the seed you give is transformed in some deterministic yet different way by each language’s implementation.

2 Likes

The way it generates float is probably different. Have you tried just integers, or ideally just bytes?

2 Likes

Ahh makes sense. That seems to have been the issue. Here’s the output from Rust and Zig. Couldn’t get the same output from Julia but that’s maybe because there’s more stuff going on in between.

use rand::prelude::*;
use std::error::Error;

fn main() -> Result<(), Box<dyn Error>> {
    let mut rng = rand::rngs::Xoshiro256PlusPlus::from_seed([0u8; 32]);
    let mut buff: [u8; 10] = [0u8; 10];
    rng.fill_bytes(&mut buff);

    eprintln!("{buff:?}");
    Ok(())
}

Outputs:

[223, 35, 11, 73, 97, 93, 23, 83, 7, 213]

And zig:

const std = @import("std");

pub fn main() !void {
    var prng_state = std.Random.Xoshiro256.init(0);
    const prng = prng_state.random();

    var buff: [10]u8 = undefined;
    prng.bytes(&buff);

    std.debug.print("{any}\n", .{buff});
}

Outputs:

{ 223, 35, 11, 73, 97, 93, 23, 83, 7, 213 }

tysm! <3