Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

std::mt19937 fails when std::uint_fast32_t is 4 bytes in GCC

The problem I have encountered occurs when I'm trying to test the cppreference example on generating pseudo-random numbers. Given the example:

#include <iostream>
#include <random>

int main() {
    std::random_device rd{};
    std::mt19937 gen{rd()};
    std::uniform_int_distribution<> dis{1, 6};

    for(int n = 0; n < 10; ++n) {
        std::cout << dis(gen) << ' ';
    }
    std::cout << '\n';
}

on my machine, it results in a crash. By "crash" I mean that the process simply hangs and returns 0xC0000005 after few seconds.

I was wondering what might be causing it. GCC bug? My machine's malfunction? I decided to test and the results were quite surprising. For example, given the following, slightly modified example:

#include <iostream>
#include <random>

int main() {
    std::random_device rd{};
    std::mt19937_64 gen{rd()}; // notice the _64 here
    std::uniform_int_distribution<> dis{1, 6};

    for(int n = 0; n < 10; ++n) {
        std::cout << dis(gen) << ' ';
    }
    std::cout << '\n';
}

The code works as expected. I tried to understand why, so I quickly ran to std::mt19937 reference, where we can see its declaration:

template<
    class UIntType, 
    size_t w, size_t n, size_t m, size_t r,
    UIntType a, size_t u, UIntType d, size_t s,
    UIntType b, size_t t,
    UIntType c, size_t l, UIntType f
> class mersenne_twister_engine;

followed by two aliases:

using mt19937 = std::mersenne_twister_engine<std::uint_fast32_t, 32, 624, 397, 31, 
                         0x9908b0df, 11, 
                         0xffffffff, 7, 
                         0x9d2c5680, 15, 
                         0xefc60000, 18, 1812433253>

and

using mt19937_64 = std::mersenne_twister_engine<std::uint_fast64_t, 64, 312, 156, 31,
                         0xb5026f5aa96619e9, 29,
                         0x5555555555555555, 17,
                         0x71d67fffeda60000, 37,
                         0xfff7eee000000000, 43, 6364136223846793005>

The interesting part is the very first template parameter for both aliases, the std::uint_fast32_t and std::uint_fast64_t. It's interesting because, diving into GCC <random> implementation, we can see that, in the line 369, the following is written:

__factor *= __detail::_Shift<_UIntType, 32>::__value;

Given the _Shift implementation at line 72:

template<typename _UIntType, size_t __w>
struct _Shift<_UIntType, __w, true> {
    static const _UIntType __value = _UIntType(1) << __w;
};

We can clearly see that an object of a type _UIntType, constructed with an argument 1 is being shifted by __w to the left. Why does that matter? Let's go back a little bit to the std::mt19937 implementation. We can see that ultimately, we will be doing:

std::uint_fast32_t(1) << 32;

which might be okay, unless...

Unless the sizeof (std::uint_fast32_t) returns 4, as it does on my machine. We are then dealing with 32 bit (assuming byte = 8 bits) unsigned integer value that is going to be shifted by 32 to the left. This is undefined behaviour and I believe this causes my program to crash.

So the question is: Is it simply a bug in some GCC implementations where sizeof (std::uint_fast32_t) == 4? Or something too clever for me is happening there and it's just my machine's malfunction?

I am using Windows 10, 64 bit, GCC 8.2 8.1.

I have asked some colleagues to run some tests and every one of them succeded (no crashes). The thing is that on their machines the expression sizeof (std::uint_fast32_t) evaluated to 8. Obviously, the UB is then gone.

EDIT: What's even more surprising is that when I seed the gen using some constant, the code behaves correctly, for example both:

std::mt19937 gen{10000000};
std::uniform_int_distribution<> dis{1, 6};

for(int n = 0; n < 10; ++n) {
    std::cout << dis(gen) << ' ';
}

,

std::mt19937 gen{5};
std::uniform_int_distribution<> dis{1, 6};

for(int n = 0; n < 10; ++n) {
    std::cout << dis(gen) << ' ';
}
std::cout << '\n';

and

std::mt19937 gen{0};
std::uniform_int_distribution<> dis{1, 6};

for(int n = 0; n < 10; ++n) {
    std::cout << dis(gen) << ' ';
}
std::cout << '\n';

fail to reproduce the SEGFAULT. I managed to alter the example a little bit. Consider the following code:

#include <iostream>
#include <random>

int main() {
    std::random_device rd{};
    auto used = rd();
    std::cout << used << '\n';
}

This code consecutively produces the output 3499211612. The thing is... This does NOT work (results in SEGFAULT):

#include <iostream>
#include <random>

int main() {
    std::random_device rd{};
    auto used = rd();
    std::cout << used << '\n';
    std::mt19937 gen{3499211612};
    std::uniform_int_distribution<> dis{1, 6};

    for(int n = 0; n < 10; ++n) {
        std::cout << dis(gen) << ' ';
    }
    std::cout << '\n';
}

While this one does:

#include <iostream>
#include <random>

int main() {
    /*std::random_device rd{};
    auto used = rd();
    std::cout << used << '\n';*/
    std::mt19937 gen{3499211612};
    std::uniform_int_distribution<> dis{1, 6};

    for(int n = 0; n < 10; ++n) {
        std::cout << dis(gen) << ' ';
    }
    std::cout << '\n';
}

Any idea how simply calling the std::random_device's operator() alters the bahaviour of the engine? I feel like I should ask another question, but I am literally unable to even put into words what is happening in the examples...

EDIT 2:

g++ -v result:

COLLECT_GCC=g++

COLLECT_LTO_WRAPPER=c:/users/felipe/desktop/mingw/mingw/bin/../libexec/gcc/x86_64-w64-mingw32/8.1.0/lto-wrapper.exe

Target: x86_64-w64-mingw32

Configured with: ../src/configure --enable-languages=c,c++ --build=x86_64-w64-mingw32 --host=x86_64-w64-mingw32 --target=x86_64-w64-mingw32 --disable-multilib --prefix=/c/temp/gcc/dest --with-sysroot=/c/temp/gcc/dest --disable-libstdcxx-pch --disable-libstdcxx-verbose --disable-nls --disable-shared --disable-win32-registry --with-tune=haswell --enable-threads=posix --enable-libgomp

Thread model: posix

gcc version 8.1.0 (GCC)

like image 205
Fureeish Avatar asked Jan 15 '19 15:01

Fureeish


1 Answers

The code you've shown is not the cause for the crash. The full definition of _Shift is:

template<typename _UIntType, size_t __w,
     bool = __w < static_cast<size_t>
          (std::numeric_limits<_UIntType>::digits)>
  struct _Shift
  { static const _UIntType __value = 0; };

template<typename _UIntType, size_t __w>
  struct _Shift<_UIntType, __w, true>
  { static const _UIntType __value = _UIntType(1) << __w; };

This uses template specialization to check the size of _UIntType at compile-time. The first version is used when __w is greater than or equal than std::numeric_limits<_UIntType>::digits, which is the case here. So the resulting value is 0, and the left-shift isn't performed.

As for the crash itself: Apparently, std::random_device doesn't work on Windows GCC and gives deterministic results (as you saw yourself). This may also be related to the cause of the crash. This question encountered a similar crash, also with GCC 8.2 on Windows.

As a workaround, you can use the Boost.Random library instead which implements the same API.

like image 63
interjay Avatar answered Nov 11 '22 12:11

interjay