What is the proper way of seeding std::mt19937 with std::chrono::high_resolution_clock inside a class?

戏子无情 提交于 2020-01-03 15:39:49

问题


First off, hello everyone! This is my first ever question here, so I hope I am not screwing up. I googled a lot before writing here. I am new to coding, to c++ and I am learning it on my own.

Considering that I was told that it is a good practice (i'm probably wrong here) to only seed any Random Engine once, what is the proper / best / more efficient way of using std::mt19937 from the random standard library inside a class, seeded by std::chrono::high_resolution_clock::now().time_since_epoch().count() from the chrono standard library?

I want to use that chrono value, because it changes really fast and it generates a hell of a creepy number. I never considered std::random_device because I think it is kinda shady. I'm probably wrong again.

EDIT: Most of the times I code and learn on my Android Phone with C4Droid IDE, because I don't have too much free time to sit on a proper computer, so that's why I think std::random_device is not really reliable.

I've done it successfully before I knew what a class is, but I am now learning classes and did a lot of trial and error (putting static_casts everywhere, trying const, static, etc, because the code was always giving errors) to get this done:

class Deck
{
private:
    std::array<Card, 52> m_card;
    const int m_seed {static_cast<int>(std::chrono::high_resolution_clock::now().time_since_epoch().count())};

    std::mt19937 m_rng {m_seed};

    int rng(int min, int max)
    {
        std::uniform_int_distribution<> rng{min, max};
    return rng(m_rng);
    }

    void swapCard(Card &a, Card &b)
    {
        Card temp {a};
        a = b;
        b = temp;
    }

public:

    Deck()
    {
        int index{0};
        for (int iii {0}; iii < Card::CS_MAX; ++iii)
        {
            for (int jjj {0}; jjj < Card::CR_MAX; ++jjj)
            {
                m_card[index] = Card(static_cast<Card::CardSuit>(iii), static_cast<Card::CardRank>(jjj));
                ++index;
            }
        }
    }

    void printDeck() const
    {
    for (int iii {0}; iii < 52; ++iii)
        {
            m_card[iii].printCard();
            if (((iii + 1) % 13 == 0) && iii != 0)
                std::cout << '\n';
            else
                std::cout << ' ';
        }
    }

    void shuffleDeck(int xTimes = 1)
    {
        for (int iii {0}; iii < xTimes; ++iii)
        {
            for (int jjj {0}; jjj < 52; ++jjj)
            {
                swapCard(m_card[jjj], m_card[rng(0, 51)]);
            }
        }
    }

};

This works, but I don't know if this is the proper way of doing it. Also, I was told that variables that never change can be made static to be shareable between all objects of the class, but I can not make m_seed static...

I am pretty sure there's a more effective way of doing this. Can you guys help?


回答1:


I was told that it is a good practice to only seed any Random Engine once

That sounds like sound advice. I'd like to add that you should preferably have exactly one generator per thread since instantiating and seeding it takes time and the standard generators are not thread safe.

I think std::random_device is not really reliable

It should be able to tell you if it is via its entropy() function. Zero entropy means its entropy pool is empty or doesn't even exist. You'll get pseudo random numbers from it in the latter case.

What is the proper way ...

By reading the links in the comments and some other tips, this is what I've collected so far:

  • Create a SeedSequence class that creates as many seed values as the generator requires. If the entropy in std::random_device is zero, combine it as best you can with some other source. I think hashed time_point samples taken some time apart could work in combination with rd() since 1 changed bit in an input value should ideally change half the bits in the hashed value.
  • Create a shared generator that is automatically instantiated and seeded when requested from a (new) thread since since generators aren't thread safe.
  • Create distribution templates that inherits from the generator so that all distributions in one thread share the same generator.
  • Don't instantiate the distribution more than necessary. If you use the same distribution a lot, keep it.

Here's an attempt at it with comments in the code:

#include <iostream>
#include <chrono>
#include <climits>
#include <functional>
#include <iterator>
#include <random>
#include <thread>
#include <type_traits>
#include <utility>

//----------------------------------------------------------------------------------
// sexmex - A hash function kindly borrowed from Pelle Evensens yet to be published
// work: http://mostlymangling.blogspot.com/
//
// g++ 8.3.1: std::hash<Integer-type> lets the value through as-is (identity)
//            so I'll use this to create proper hash values instead.
template<typename Out = size_t, typename In>
inline std::enable_if_t<sizeof(In) * CHAR_BIT <= 64 &&
                            std::numeric_limits<Out>::is_integer &&
                            std::numeric_limits<In>::is_integer,
                        Out>
sexmex(In v) {
    uint64_t v2 = static_cast<uint64_t>(v); // cast away signedness
    v2 ^= (v2 >> 20) ^ (v2 >> 37) ^ (v2 >> 51);
    v2 *= 0xA54FF53A5F1D36F1ULL; // Fractional part of sqrt(7)
    v2 ^= (v2 >> 20) ^ (v2 >> 37) ^ (v2 >> 51);
    v2 *= 0x510E527FADE682D1ULL; // Fractional part of sqrt(11)
    v2 ^= (v2 >> 20) ^ (v2 >> 37) ^ (v2 >> 51);
    // Discard the high bits if Out is < 64 bits. This particular hash function
    // has not shown any weaknesses in the lower bits in any widely known test
    // suites yet.
    return static_cast<Out>(v2);
}
//----------------------------------------------------------------------------------
class seeder {
public:
    using result_type = std::uint_least32_t;

    // function called by the generator on construction to fill its internal state
    template<class RandomIt>
    void generate(RandomIt Begin, RandomIt End) const noexcept {
        using seed_t = std::remove_reference_t<decltype(*Begin)>;
        std::random_device rd{};

        if(rd.entropy() == 0.) { // check entropy
            // zero entropy, add some
            constexpr auto min = std::chrono::high_resolution_clock::duration::min();
            std::vector<seed_t> food_for_generator(
                static_cast<size_t>(std::distance(Begin, End)));

            for(int stiring = 0; stiring < 10; ++stiring) {
                for(auto& food : food_for_generator) {
                    // sleep a little to ensure a new clock count each iteration
                    std::this_thread::sleep_for(min);
                    std::this_thread::sleep_for(min);
                    auto cc = std::chrono::high_resolution_clock::now()
                                  .time_since_epoch()
                                  .count();
                    food ^= sexmex<seed_t>(cc);
                    food ^= sexmex<seed_t>(rd());
                }
                stir_buffer(food_for_generator);
            }

            // seed the generator
            for(auto f = food_for_generator.begin(); Begin != End; ++f, ++Begin)
                *Begin = *f;

        } else {
            // we got entropy, use random_device almost as-is but make sure
            // values from rd() becomes seed_t's number of bits and unbiased
            // via sexmex.
            //
            // seed the generator
            for(; Begin != End; ++Begin) *Begin = sexmex<seed_t>(rd());
        }
    }

private:
    template<typename SeedType>
    inline void stir_buffer(std::vector<SeedType>& buf) const noexcept {
        for(size_t i = 0; i < buf.size() * 2; ++i) {
            buf[i % buf.size()] += static_cast<SeedType>(
                sexmex(buf[(i + buf.size() - 1) % buf.size()] + i));
        }
    }
};
//----------------------------------------------------------------------------------
struct shared_generator {
    // we want one instance shared between all instances of uniform_dist per thread
    static thread_local seeder ss;
    static thread_local std::mt19937 generator;
};

thread_local seeder shared_generator::ss{};
thread_local std::mt19937 shared_generator::generator(ss);
//----------------------------------------------------------------------------------
// a distribution template for uniform distributions, both int and real
template<typename T>
class uniform_dist : shared_generator {
public:
    uniform_dist(T low, T high) : distribution(low, high) {}

    // make instances callable
    inline T operator()() { return distribution(generator); }

private:
    template<class D>
    using dist_t =
        std::conditional_t<std::is_integral_v<D>, std::uniform_int_distribution<D>,
                           std::uniform_real_distribution<D>>;

    dist_t<T> distribution;
};
//----------------------------------------------------------------------------------
void thread_func() {
    uniform_dist<int> something(0, 10);
    for(int i = 0; i < 10; ++i) std::cout << something() << "\n";
}

int main() {
    // all distributions sharing the same generator:
    uniform_dist<size_t> card_picker(0, 51);
    uniform_dist<int16_t> other(-32768, 32767);
    uniform_dist<float> fd(-1000.f, 1000.f);
    uniform_dist<double> dd(-1., 1.);
    for(int i = 0; i < 10; ++i) std::cout << card_picker() << "\n";
    std::cout << "--\n";
    for(int i = 0; i < 10; ++i) std::cout << other() << "\n";
    std::cout << "--\n";
    for(int i = 0; i < 10; ++i) std::cout << fd() << "\n";
    std::cout << "--\n";
    for(int i = 0; i < 10; ++i) std::cout << dd() << "\n";
    // in the thread function, a new generator will be created and seeded.
    std::thread t(thread_func);
    t.join();
}


来源:https://stackoverflow.com/questions/56242943/what-is-the-proper-way-of-seeding-stdmt19937-with-stdchronohigh-resolution

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!