OpenMP und C++11 Zufallszahlengenerator



  • Hallo ihr Lieben,

    durch einen anderen Thread wurde ich angeregt einen Teil meines Quellcodes neu zu überdenken.

    Ich bin von C++11 und seinen Zufallszahlengeneratoren begeistert, die sich einfach kopieren lassen. Im Beispiel initialisiere ich eine Klasse und gebe mittels firstprivate jedem Thread eine eigene Kopie. Da ich in RNG einen Kopierkonstruktor habe geht das ohne Probleme.

    Das Problem ist nun, dass durch das Kopieren der Zustand des Generators (von dem kopiert wurde) nicht verändert wird! Wenn ich das ganze parallelisierte Prozedere wiederhole sehe ich die gleichen Zufallszahlen - klar!

    Aber wie kann ich das vermeiden? Als quick und dirty Lösung dachte ich daran den Zufallsgenerator einfach in dem parallelisierten Block anhand der Zeit zu initialisieren, also jedes Mal mit neuem Seed zu initialiseren. Finde ich aber unschön.

    Ich muss also den Kopierkonstrutor so schreiben, dass er ... ja was? Einen Pointer verwendet? Auf dass jede Verwendung in dem pragma Block tatsächlich den Zustand von rng verändert? Ist das dann thread safe?

    #ifndef RNG_H
    #define RNG_H
    
    #include <iostream>
    #include <random>
    #include <vector>
    
    #include "omp.h"
    
    using namespace std;
    
    class RNG
    {
    	public:
    		RNG(int);
    		RNG(const RNG&);
    
    		auto operator()()	-> double;
    
    		RNG& operator=(const RNG&)	= delete;
    		RNG(RNG&&)									= delete;
    		RNG& operator=(RNG&&)				= delete;
    
    	private:
    		vector<mt19937> _engines;
    		uniform_real_distribution<double> _distribution;
    
    };
    #endif
    
    #include "rng-omp.h"
    
    /* constructor */
    RNG::RNG( int threads ): _engines(), _distribution( 0.0 , 1.0 )
    {
    	for(int i = 0; i < threads; ++i)
    		_engines.push_back( mt19937( i ) );
    }
    
    /* copy constructor */
    RNG::RNG(const RNG& rhs):
    	_engines( rhs._engines ), _distribution( rhs._distribution )
    {}
    
    /* get uniform distributed random number in (0,1) */
    auto RNG::operator()() -> double
    {
    	return _distribution( _engines[ omp_get_thread_num() ] );
    }
    
    #include <iostream>
    
    #include "rng-omp.h"
    
    int main()
    {
    	int const threads = 8;
    
    	omp_set_num_threads( threads );
    
    	RNG rng(threads);
    
    	#pragma omp parallel firstprivate( rng )
    	{
    		#pragma omp critical
    		{
    			cout	<< "random number from thread "
    						<< omp_get_thread_num()	<<	": "	<< rng()
    						<< endl;
    		}
    	}
    
    	cout << "\nNext sequence!\n" << endl;
    
    	#pragma omp parallel firstprivate( rng )
    	{
    		#pragma omp critical
    		{
    			cout	<< "random number from thread "
    						<< omp_get_thread_num()	<<	": "	<< rng()
    						<< endl;
    		}
    	}
    
    	return 0;
    }
    

    Ausgabe

    random number from thread 0: 0.592845
    random number from thread 5: 0.0551801
    random number from thread 2: 0.185082
    random number from thread 1: 0.997185
    random number from thread 6: 0.947476
    random number from thread 3: 0.0707249
    random number from thread 4: 0.900621
    random number from thread 7: 0.227339
    
    Next sequence!
    
    random number from thread 4: 0.900621
    random number from thread 2: 0.185082
    random number from thread 1: 0.997185
    random number from thread 5: 0.0551801
    random number from thread 3: 0.0707249
    random number from thread 0: 0.592845
    random number from thread 6: 0.947476
    random number from thread 7: 0.227339
    

    Gruß,
    -- Klaus.



  • Das ganze System sieht mir nicht so sinnvoll aus.

    Wenn du Zufallszahlen willst, wieso erzeugst du dir nicht in jedem eigenen Thread einen Generator? Wieso willst du die Zusammenfassen?

    Dann: In deinem Code hättest du eigentlich eine schöne Abgrenzung, mehrere Generatoren für mehrere Threads. Aber dann nutzt du im double operator() eine OMP-Funktion, die dir die schöne Abgrenzung kaputt macht und die Kopplung erhöht.



  • Hallo Skym0sh0,

    Skym0sh0 schrieb:

    Wenn du Zufallszahlen willst, wieso erzeugst du dir nicht in jedem eigenen Thread einen Generator? Wieso willst du die Zusammenfassen?

    Dann: In deinem Code hättest du eigentlich eine schöne Abgrenzung, mehrere Generatoren für mehrere Threads. Aber dann nutzt du im double operator() eine OMP-Funktion, die dir die schöne Abgrenzung kaputt macht und die Kopplung erhöht.

    Okay,
    ich gebe jedem Thread einen eigenen (neu entworfenen) Zufallszahlengenerator. Jetzt habe ich aber nach wie vor das Problem, dass die erste und zweite Sequenz dieselben Zufallszahlen liefern.
    Diese nacheinander aufgerufenen parallelen Blöcke stehen symbolisch für eine immer wieder aufgerufene Funktion, die einen Datensatz mit Zufallszahlen bearbeiten.

    Irgendwie muss ich doch aber dafür sorgen, dass wiederholte Aufrufe derselben Funktion mit neuen Sequenzen von Zufallszahlen versorgt werden. 😕

    Gruß,
    -- Klaus.

    #ifndef RNG_H
    #define RNG_H
    
    #include <iostream>
    #include <random>
    
    #include "omp.h"
    
    using namespace std;
    
    class RNG
    {
    	public:
    		RNG(int seed):
    			_engine( seed ), _distribution( 0.0, 1.0 ){};
    
    		double operator()(){ return _distribution( _engine ); }
    
    	private:
    		mt19937 _engine;
    		uniform_real_distribution<double> _distribution;
    
    };
    #endif
    
    #include <iostream>
    
    #include "rng.h"
    
    int main()
    {
    	int const threads = 8;
    
    	omp_set_num_threads( threads );
    
    	#pragma omp parallel
    	{
    		RNG rng( omp_get_thread_num() );
    
    		#pragma omp critical
    		{
    			cout	<< "random number from thread "
    						<< omp_get_thread_num()	<<	": "	<< rng()
    						<< endl;
    		}
    	}
    
    	cout << "\nNext sequence!\n" << endl;
    
    	#pragma omp parallel
    	{
    		RNG rng( omp_get_thread_num() );
    
    		#pragma omp critical
    		{
    			cout	<< "random number from thread "
    						<< omp_get_thread_num()	<<	": "	<< rng()
    						<< endl;
    		}
    	}
    
    	return 0;
    }
    


  • Klaus82 schrieb:

    ich gebe jedem Thread einen eigenen (neu entworfenen) Zufallszahlengenerator. Jetzt habe ich aber nach wie vor das Problem, dass die erste und zweite Sequenz dieselben Zufallszahlen liefern.

    Verwende unterschiedliche Seeds, z.B. von std::random_device .



  • Nexus schrieb:

    Klaus82 schrieb:

    ich gebe jedem Thread einen eigenen (neu entworfenen) Zufallszahlengenerator. Jetzt habe ich aber nach wie vor das Problem, dass die erste und zweite Sequenz dieselben Zufallszahlen liefern.

    Verwende unterschiedliche Seeds, z.B. von std::random_device .

    Was es nicht alles gibt.

    Na ja, am Ende würde ich damit genau das tun, was ich oben als quick and dirty bezeichnet habe: Der seed wird jedes Mal zufällig bestimmt. Was natürlich super für die Reproduzierbarkeit ist. 😉

    #ifndef RNG_H
    #define RNG_H
    
    #include <iostream>
    #include <random>
    
    #include "omp.h"
    
    using namespace std;
    
    class RNG
    {
    	public:
    		RNG():
    			_engine( _rd() ), _distribution( 0.0, 1.0 ){};
    
    		double operator()(){ return _distribution( _engine ); }
    
    	private:
    		random_device _rd;
    		mt19937 _engine;
    		uniform_real_distribution<double> _distribution;
    
    };
    #endif
    

    Gruß,
    -- Klaus.



  • Du erzeugst deine RNGs in jedem parallel-Block aufs neue, und das mit den selben Seeds. Klar, daß immer wieder dieselben Sequenzen herauskommen. Außerdem ist der Mersenne-Twister m.W. nicht gerade billig zu konstruieren.

    Wie wär's mit einem übergeordneten vector<RNG> mit threads RNGs, auf die Du dann mit rng_vec[omp_get_thread_num()]() zugreifst?



  • Caligulaminus schrieb:

    Wie wär's mit einem übergeordneten vector<RNG> mit threads RNGs, Auf die Du dann mit rng_vec[omp_get_thread_num()]() zugreifst?

    Du meinst so wie in meinem Startpost wofür ich schon abgewatscht wurde? 😕

    Gruß,
    -- Klaus.



  • In deinem Startpost war RNG von OMP abhängig.



  • Caligulaminus schrieb:

    In deinem Startpost war RNG von OMP abhängig.

    Ja.

    Aber ist das jetzt nicht eine konzeptionelle Frage? Ob ich einen Vektor von RNGs habe oder einen wrapper (genannt RNG) um einen Vektor von Generatoren lege?

    Das macht doch nicht viel Unterschied?

    Gruß,
    -- Klaus.



  • Klaus82 schrieb:

    Was natürlich super für die Reproduzierbarkeit ist. 😉

    Wenn du reproduzierbare Zufallszahlen willst, dann nimm eben zuvor festgelegte Seeds. Naheliegend, oder? 🙂

    P.S. Ich würde nicht jedes Mal auf den Vektor zugreifen. Übergib jedem Thread seinen eigenen, unabhängigen RNG, nachdem er entsprechend geseedet wurde. Gerade in Multithreaded-Szenarien würde alles so modular wie möglich halten und nur über sehr kleine Schnittstellen miteinander kommunizieren.



  • Ja, ja ... danke für die ganzen Vorschläge.

    [piensen]
    Ich befürchte nur, dass es am Ende sowieso niemanden interessiert, wie penibel ich darauf achte. D.h. ich kann eigentlich auch direkt jedes Mal mit std::random_device seeden. 😞
    [/piensen]



  • So,

    ich habe jetzt eine große #pragma omp parallel Umgebung, auf dass jeder Thread seinen eigenen Kram macht und jeder vorkommende PRNG wird mittels std::random_device initialisiert.

    Ist die mathematisch schändlichste Lösung, aber dafür die praktikabelste. 🙄

    Gruß,
    -- Klaus.


  • Mod

    Klaus82 schrieb:

    Ist die mathematisch schändlichste Lösung, aber dafür die praktikabelste. 🙄

    Schändlich ist das nur, wenn es nicht das ist, was du möchtest. Wenn es nicht das ist, was du möchtest, warum machst du es dann so?

    Wenn du auf die Reproduzierbarkeit anspielst: Erstens ist es ja wohl einfach, sich dafür eine Lösung zu denken, zweitens hat Nexus diese bereits ausdrücklich erklärt.



  • Sorry für die späte Rückmeldung.

    Ich habe tatsächlich nochmal den Eintrag bei Stackoverflow gefunden, wo die Generatoren im Vektor gespeichert werden und jeder Thread auf einen Eintrag zugreift - so wie ich es im Startpost geschrieben hatte.

    Also scheint das doch 'okay' zu sein, oder nicht?

    Gruß,
    -- Klaus.


Anmelden zum Antworten