It looks like the rack modules all use the global random state, and use calls like rack::random::uniform() to generate randoms from it.
Do any modules keep their own local random state, rather than using the global one? What are the pros and cons? Is there an API in rack::random where you can (easily) keep your own random state?
Not a clue, but then I don’t understand what you mean by global random state. I’ve tried to look into some of the random API code and documentation, and there was a bunch I did not understand. So I would love if you could give some background.
Every pseudo random generator needs some state. When it generates a new number it uses that state, then updates it.
The docs for VCV say that while they used to use thread local storage to keep state, they now use a single global state for all threads. Which sounds like a good improvement.
But that means if I generate a random number, and then you do, the next one I generate will come from the state that you updated. Which may or may not be what I expect.
Some random number generators let you provide your own state that others can’t change. For example, many of the c++ standard generators let you / force you to do this.
So I just want to know what the options are for those who choose to use rack::random.
If all you care about is getting a random number, than just having one global RNG state is fine. A pseudo random generator gives back a number every call, but if there is more than one consumer it just means that you’re getting pseudo-random numbers R[n] R[n+m] where m = the number consumers since the last sample consumed by the current thread.
I don’t know all the theory behind RNGs, but I think that R[n] R[n+1] R[n+3] etc is just as random as R[n] R[n+m] R[n+p].
I’m interested in a use case where it matters.
The only thing I don’t understand is if there’s one global RNG state, doesn’t it require a lock to avoid races between threads? Otherwise there’s a possibility for race conditions where more than one thread is updating the RNG state at the same time.
I don’t know if that would break anything, but it does mean that whatever operation produces the next random number won’t behave the same single- and multi-threaded.
Easy enough to use a state per thread, and the usual psuedo-random algorithms aren’t that computationally expensive.
Can you point to the documentation you are referring to? I think there must be some misstatement or some misunderstanding.
There are a number of modules that allow you to reseed the uniform RNG, including my Rhythm Explorer. Multiple copies can use the same seed, and be reset independently, and each instance will properly generate the same sequence without interfering with the other. I don’t think that would work if what you are saying is true.
Yeah, but what about R[n], R[n+k], R[n + 2k]? If k were perversely the size of the “state”, then I those R number would all be identical, not at all random.
Do you use rack::random for this? Obviously random::get() can’t be “seed specific”? The thing I’m talking about is in the release notes. Look at that last entry for 2.2.0.
2.2.0 (2022-11-23)
Add MIDI Loopback driver, allowing modules with MIDI ports to send MIDI messages to other modules.
Improve fuzzy search in module browser.
Allow building on ARM64 CPUs.
Rename plugin packages to SLUG-VERSION-OS-CPU.vcvplugin.
Rack Pro
Add VST3, Audio Unit, and CLAP plugin adapters.
Add framerate setting to plugins.
API
Add system::sleep().
Make random::get(), uniform(), etc use global random state instead of thread-local.
Ahhh… It looks like Rack maintains a static Xoroshiro128Plus rng that all the get(), normal(), uniform(), etc. functions access.
So it does look like all instances that use random::get(), random::uniform() etc. do share a single state. I think generally this does not cause a problem because the api does not give a convenient function to reseed the static rng. But the api does have a random::local() that returns the static rng&, and from there you should be able to call seed(). That does seem like it could cause problems.
But I don’t use the static instance, but instead I declare my own local instance of Xoroshiro128Plus, also called rng. I use rng.seed() and rng() directly. I never use the global copy of rng.
Some of mine keep their own state; those that do don’t usually call Rack’s random. Why? Because I like PCG better than xorshifts.
You can easily keep your own random state, with Rack’s RNG flavor, by creating your own instance of Xoroshiro128Plus (take a look at random.hpp and random.cpp).