Pretty much any implementation of “old” rand()
use an LCG; while they are generally not the best generators around, usually you are not going to see them fail on such a basic test – mean and standard deviation is generally got right even by the worst PRNGs.
Common failings of “bad” – but common enough – rand()
implementations are:
- low randomness of low-order bits;
- short period;
- low
RAND_MAX
; - some correlation between successive extractions (in general, LCGs produce numbers that are on a limited number of hyperplanes, although this can be somehow mitigated).
Still, none of these are specific to the API of rand()
. A particular implementation could place a xorshift-family generator behind srand
/rand
and, algoritmically speaking, obtain a state of the art PRNG with no changes of interface, so no test like the one you did would show any weakness in the output.
Edit: @R. correctly notes that the rand
/srand
interface is limited by the fact that srand
takes an unsigned int
, so any generator an implementation may put behind them is intrinsically limited to UINT_MAX
possible starting seeds (and thus generated sequences). This is true indeed, although the API could be trivially extended to make srand
take an unsigned long long
, or adding a separate srand(unsigned char *, size_t)
overload.
Indeed, the actual problem with rand()
is not much of implementation in principle but:
- backwards compatibility; many current implementations use suboptimal generators, typically with badly chosen parameters; a notorious example is Visual C++, which sports a
RAND_MAX
of just 32767. However, this cannot be changed easily, as it would break compatibility with the past – people usingsrand
with a fixed seed for reproducible simulations wouldn’t be too happy (indeed, IIRC the aforementioned implementation goes back to Microsoft C early versions – or even Lattice C – from the mid-eighties); -
simplistic interface;
rand()
provides a single generator with the global state for the whole program. While this is perfectly fine (and actually quite handy) for many simple use cases, it poses problems:- with multithreaded code: to fix it you either need a global mutex – which would slow down everything for no reason and kill any chance of repeatability, as the sequence of calls becomes random itself -, or thread-local state; this last one has been adopted by several implementations (notably Visual C++);
- if you want a “private”, reproducible sequence into a specific module of your program that doesn’t impact the global state.
Finally, the rand
state of affairs:
- doesn’t specify an actual implementation (the C standard provides just a sample implementation), so any program that is intended to produce reproducible output (or expect a PRNG of some known quality) across different compilers must roll its own generator;
- doesn’t provide any cross-platform method to obtain a decent seed (
time(NULL)
is not, as it isn’t granular enough, and often – think embedded devices with no RTC – not even random enough).
Hence the new <random>
header, which tries to fix this mess providing algorithms that are:
- fully specified (so you can have cross-compiler reproducible output and guaranteed characteristics – say, range of the generator);
- generally of state-of-the-art quality (from when the library was designed; see below);
- encapsulated in classes (so no global state is forced upon you, which avoids completely threading and nonlocality problems);
… and a default random_device
as well to seed them.
Now, if you ask me I would have liked also a simple API built on top of this for the “easy”, “guess a number” cases (similar to how Python does provide the “complicated” API, but also the trivial random.randint
& Co. using a global, pre-seeded PRNG for us uncomplicated people who’d like not to drown in random devices/engines/adapters/whatever every time we want to extract a number for the bingo cards), but it’s true that you can easily build it by yourself over the current facilities (while building the “full” API over a simplistic one wouldn’t be possible).
Finally, to get back to your performance comparison: as others have specified, you are comparing a fast LCG with a slower (but generally considered better quality) Mersenne Twister; if you are ok with the quality of an LCG, you can use std::minstd_rand
instead of std::mt19937
.
Indeed, after tweaking your function to use std::minstd_rand
and avoid useless static variables for initialization
int getRandNum_New() {
static std::minstd_rand eng{std::random_device{}()};
static std::uniform_int_distribution<int> dist{0, 5};
return dist(eng);
}
I get 9 ms (old) vs 21 ms (new); finally, if I get rid of dist
(which, compared to the classic modulo operator, handles the distribution skew for output range not multiple of the input range) and get back to what you are doing in getRandNum_Old()
int getRandNum_New() {
static std::minstd_rand eng{std::random_device{}()};
return eng() % 6;
}
I get it down to 6 ms (so, 30% faster), probably because, unlike the call to rand()
, std::minstd_rand
is easier to inline.
Incidentally, I did the same test using a hand-rolled (but pretty much conforming to the standard library interface) XorShift64*
, and it’s 2.3 times faster than rand()
(3.68 ms vs 8.61 ms); given that, unlike the Mersenne Twister and the various provided LCGs, it passes the current randomness test suites with flying colors and it’s blazingly fast, it makes you wonder why it isn’t included in the standard library yet.