Skip to content

Instantly share code, notes, and snippets.

@ryanmr

ryanmr/rng_rust.md

Last active Feb 28, 2021
Embed
What would you like to do?

I am using a SeedableRng to make reproducible simulations.

Here's my get_rng function:

use rand::prelude::*;
use rand_xoshiro::rand_core::SeedableRng;
use rand_xoshiro::Xoshiro256StarStar;

pub fn get_rng() -> impl Rng {
    // xoshiro; global rng
    // this is how the original worked
    // and we repeat that here
    let rng1 = Xoshiro256StarStar::from_seed([
        18, 25, 1, 14, 18, 1, 13, 16, 5, 18, 19, 1, 4, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0,
        0, 0, 0, 0,
    ]);
    return rng1;
}

This portion works great.

My simulation function, roughly:

pub fn simulation(&mut self, input: &Vec<SimInput>, rng: &mut dyn RngCore) -> () {
  // ...
  self.integrate_step(input, &opts, rng);
  // ...
}

pub fn integrate_step(
        &self,
        input: &Vec<SimInput>,
        opts: &SimOpts,
        rng: &mut dyn RngCore,
    ) -> f64 {
  // ...
}

This will also forward the rng to some other methods.

The integrate_step uses StandardNormal and Standard samples:

let n01: f64 = rng.sample(StandardNormal);
let u01: f64 = rng.sample(Standard);
// ... math with n01 and u01

Now the question!

I want to abstract out all of the sample StandardNormal and Standard and the crazy &mut dyn RngCore out from the simulation code.

I came up with this:

pub struct RngProvider {
    rng: Box<dyn RngCore>, // <--- box
}

impl RngProvider {
    pub fn new() -> RngProvider {
        let b = Box::new(get_rng());

        return RngProvider { rng: b };
    }

    /// gets standard
    pub fn u01(&mut self) -> f64 {
        return self.rng.sample(Standard);
    }

    /// gets standard normal
    pub fn n01(&mut self) -> f64 {
        return self.rng.sample(StandardNormal);
    }
}

Is this good?

Then it's used like:

// previously in a bootstrap file ...
let mut rng_provider = RngProvider::new();

pub fn simulation(&mut self, input: &Vec<OuInput>, rng: &mut RngProvider) -> () {
    // ...
}

// later in integrate_step...
let n01: f64 = rng.n01();
let u01: f64 = rng.u01();

Performance:

  • The RngCore version is run time 5833 ms
  • The RngProvider version is run time 8300 ms
  • Individual compute steps have a ~200 ms difference, and that will grow as the input grows.

The nicer abstraction is 30% slower, with the only change being this new struct and the alternative calls. This is run on the same SimInput, same machine, in release mode, repeatedly.

I think it is because of the Box<dyn RngCore> in the struct. I am not sure what I can do as an alternative.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment