Search code examples
c++unit-testingrandom-seed

Is it safe to use std::mt19937 (with fixed seed) within unit tests?


I had written a unit test of a function that requires a Random Number Generator (RNG). The implementation of the tested function starts with the following lines:

const int nSeed = 0;
std::mt19937 randNbGen(nSeed);

and the randNbGen object is later used within that same function to perform calculations that eventually yield its return value.

By seeding the RNG, it generates the same sequence of numbers from a run to the next, which I need for the unit test to be consistent. Also, I assumed that by using an RNG from the std library (std::mt19937 in my case) I was guaranteed that the sequence of numbers generated wouldn’t be changing in the future.

However: 6 months later (ie now) the test fails. I checked out a previous commit that used to pass the test, now it fails the test with the same function output as it currently does. I also note that randNbGen generates the same sequence of numbers in both commits.

Is it indeed possible that the RNG (implementation) changed? Or do I must have skrewed up somewhere?
What is the recommended way to handle this situation?
Bonus question: is the RNG's behavior constant accross platforms?

PS: I'm using MSVC on Windows (Visual Studio 2022)


Solution

  • This answer is a summary of multiple contributions from various users that you will find in the comments of the original question: thanks to all who helped!

    Recall that the C++ standard offers:

    • Three main Random Number Generators (RNGs):
      Generate sequences of (pseudo-)random numbers.
      • std::linear_congruential_engine (includes std::minstd*)
      • std::mersenne_twister_engine (includes std::mt19937*)
      • std::subtract_with_carry_engine (includes std::ranlux*)
    • Random number distributions:
      Work "on top" of an RNG and allow to randomly generate numbers following a given distribution (uniform, normal, etc.).
    • More things, see here.

    Also recall that we have three main implementations of the C++ library (amongst others):

    • MSVC’s MS STL
    • gcc’s libstdc++
    • clang’s libc++

    A first aspect of the question is whether the behavior of the Random Number Generator (RNG) may change through time:

    • The behavior of the the 3 RNGs is strictly defined in the C++ standard. In the case of the std::mt19937 RNG, it requires the 10,000th iteration of a default-constructed std::mt19937 to produce a given value (Pete Becker). In practice, it means we can assume that for a given seed, the sequence of number generated should be the same now and forever, for any implementation compliant to the standard.
    • However, in the case of distributions, the C++ standard does not guarantee that a particular sequence of numbers will be generated, it only has to comply with the shape of the distribution. In particular, the sequence of number will change between implementations, and for a given implementation, it can change in time ie across versions (Alan Birtles, user17732522). This change in behavior is documented in several other answers, for example here and there.

    As a consequence:

    • It is safe to write unit tests assuming that the behavior of std::mt19937 or any of the other two RNG families will be unchanged in time.
    • On the other hand, unit tests written based on distributions are likely to be failing in the future.

    In my particular case, even though I thought I was using std::mt19937 directly, it turned out that I was actually using std::uniform_int_distribution<>. That is why unit tests that used to pass were now failing. Actually, rolling back from Visual Studio 2022 v17.7.4 to Visual Studio 2022 v17.4.2 made the tests pass. Hence it appears that the implementation of std::uniform_int_distribution<> changed in Visual Studio 2022 around version 17.5~17.6, as can be seen here and there. For instructions on how to rollback Visual Studio, see here.

    Can I use the distributions within unit tests, despite the risk that the sequence of numbers outputted might change?
    Yes, but consider the following precautions:

    • One could prefer using Boost’s implementations of the distributions in the boost::random library instead of a standard library implementation. When developing cross-platform software, it has the advantage of being the same implementation independently of the platform.
    • One could write a test that assesses that the sequence of numbers generated by the distributions remains unchanged eg using the already implemented operator<<() for std::mt19937 (François Andrieux). In the contrary, such test(s) would act as an early alert indicating that the other failing unit tests are due to changes in implementation of the distribution, not the code itself.
    • Once it is detected that the sequence of number issued by the distribution changed, alter the expected values of the unit tests accordingly, until next change in the distribution’s implementation…

    Alternative solutions:

    • One could write tests that run several times and test the distribution of the randomly obtained outputs eg their mean and/or standard deviation. (Note: not sure how convenient that would be, because that might slow down the test considerably, which is not desirable.)
    • Write our own distributions based on the C++ standard RNGs, this way it is easier to guarantee that their behavior does not change in time (Red.Wave) . (Note: it is also easy to implement them incorrectly…)
    • If you do not require the random sequence to be different from a run to the next, then you could generate a static sequence of numbers from a distribution, store it eg in a header and use it ad aeternam. (Peter - Reinstante Monica)