Idiomatic Rust to port C++ mutable static functionality

Hi all,

I'm porting the ran2() function from Numerical Recipes in C (can be found here, at page 282) into Rust. I've never written any C or C++ before today, so I apologise if I get anything from here on incorrect.

Within the C++ ran2() are three (mutable) static variables:

static long idum2 = 123456789;
static long iy = 0;
static long iv[NTAB];

My understanding is that, being statics, they are initalised on first call to the function with the starting values above. Then for every call to ran2() thereafter they use values from previously, and update each iteration accordingly. The input parameter, idum, also updates every iteration, so I also needed to port this into Rust (preferably without using a pointer?).

It appears the use of mutable statics in Rust is highly discouraged. That said, I still wanted to retain the exact functionality of ran2(). From my limited testing, I have successfully done so by teasing out the statics, and idum, into a struct as follows:

/*
main.rs
*/
mod random; // contains ran2()

// would normally be in random.rs but we need it here to create `iv`
const NTAB: i32 = 32;

// struct to keep track of the statics in the C++ code
struct Ran2Params {
    idum: i32
    idum2: i32,
    iy: i32,
    iv: [i32; NTAB as usize],
}

fn main() {
    // instantiate our params for ran2()
    let mut params: Ran2Params = Ran2Params {
        idum: -5,
        idum2: 123456789,
        iy: 0,
        iv: [0; NTAB as usize],
    };

    let mut x: f64;

    // generate a random number, x, 10 times
    for _ in 0..10 {
        // generate x and store the new params temporarily
        let (idum, idum2, iy, iv, x) = random::ran2(
            params.idum, params.idum2, params.iy, params.iv);
        // update the struct with the new params before they go out of scope
        params = Ran2Params {
            idum: idum,
            idum2: idum2,
            iy: iy,
            iv: iv,
        };
        // check the value of x
        println!("x = {}", x);
    }
}

random.rs itself looks like a Rust-ified version of the original ran2() function, but with the static variables omitted. I won't include it here for brevity, but here's a pastebin with the code.

Is there a better way to do this, such as using lazy_static? Passing around temporary variables and updating a struct every time I generate a new random number feels clumsy, especially if I need to generate N (= large) random numbers. Thanks!

It's not. Random numbers usually come from a stateful source of randomness. Using local variables instead of the global static state is exactly what you should do.

Compare the APIs of the rand crate.

6 Likes

You did exactly the thing you're supposed to do.

One thing that will make it more ergonomic however is to design your API more like this:

use random::Ran2Generator;

// Get new generator with initial values
let mut rng = Ran2Generator::new();
// This is basically a call to the old `ran2()`
let value = rng.next();

I.e. move the struct into the random module, and define methods on it so that consumers don't need to know about the fields, just like the original.

Worth noting is that, unlike in the original C++, you'll have to keep a single rng alive and pass it around everywhere. If that seems too unruly, you can stuff it in a lazy_static (via the crate you found), or in a thread_local.

Either choice will require using some sort of wrapper to allow interior mutability. Lazy statics are shared between threads so rust will require you to wrap it a synchronization primitive like Mutex. For a thread local you can try wrapping it in a RefCell, or alternatively define fn next to take &self and wrap each integer field in Cell. (RefCell is the more common and more general approach, while Cells may be slightly faster)

3 Likes

Note that in C++ statics in root scope have different semantics with statics within function body. While the latter behaves as you said, statics in root scope behaves like the ones in C and Rust - their initial bit patterns are calculated in build time and packed with the binary, and initialized as-is when the program loaded by the OS.

Also note that before C++11 initializing those local statics concurrently between threads was UB, but since C++11 it's guaranteed only one thread will initialize the static, by using atomics or locks or whatever underneath. It doesn't means mutating it in parallel is safe though.

3 Likes

Thanks for this. I think by 'clumsy' in my original post I was trying to express that keeping track of the struct and creating a use-once const value in main() felt messy. I followed exactly what you said to do and it's working perfectly. I suppose I was asking more about 'code cleanup' than I was about porting the functionality itself.

To be pedantic, C++ will init statics that are inited with constant expressions before main runs and before initing statics inited to non-constant expressions. Only the constant expression ones behave as you describe. C++ let’s you write at top level scope static int x = time_to_ping_crates_io(); which will run after constant expression statics are initialized but before main.

Since they're just numbers why not have them be AtomicU64s?

My understanding is that std::sync::Ordering only applies to the same atomic and doesn't introduce any ordering relationship between different atomic variables, so wouldn't you get (the equivalent of) torn reads that way?

Imagine one thread updates idum while another thread which is also using the RNG updates iv, and now the RNG's internal state is all messed up.

1 Like

Regarding the ordering between different variables, all operations performed with the SeqCst ordering are guaranteed to have a single consistent ordering even if performed on separate atomic variables.

1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.