Kopieraufwand vermeiden

Die bislang vorgestellte Lösung ist nicht sehr günstig. Zwar erzeugen wir nur noch neue Generatoren, wenn dies sich nicht vermeiden lässt, jedoch kopieren wir diese unnötig hin und her. Werfen wir beispielsweise einen Blick auf den Fall in der Methode get, bei der ein bereits vorhandenes Objekt ausgeliehen wird:

T rg = unused.front();
unused.pop_front();
return rg;

Hier wird das Objekt aus der Liste in eine lokale Variable umkopiert. Und bei der Rückgabe sieht es ebenso ungünstig aus:

void free(T engine) {
   std::lock_guard<std::mutex> lock(mutex);
   unused.push_back(engine);
}

Hier wird der zurückgegebene Generator zunächst bei der Parameterübergabe kopiert und dann möglicherweise ein weiteres Mal bei dem Aufruf von push_back.

Naheliegend wäre die Verwendung von Zeigern, da dann nur Zeiger kopiert werden müssten und nicht versehentlich der gesamte Inhalt eines Generators, der bei std::mt19937 sehr umfangreich ist. Wenn jedoch Zeiger direkt verwendet werden, bleibt immer die Frage, wer die Verantwortung für die Freigabe des Speichers trägt. In C++ will man das nicht dem Zufall überlassen, sondern die Verantwortung immer nach dem Prinzip des RAII einem konkreten Objekt überlassen, dessen Destruktor sich darum kümmert. Theoretisch könnten dann smart pointer wie etwa std::shared_ptr<T> zum Einsatz kommen. Aber die kommen nicht kostenfrei, hier wäre immer die zusätzliche Indirektion zu zahlen und dies bei jedem Abruf eines Werts vom Generator.

Seit C++11 gibt es hier eine alternative Vorgehensweise, die auf der Verschiebesemantik (move semantics) beruht. Die prinzipielle Idee ist, dass die Verantwortung für einen Datenbestand von einem Objekt zu einem anderen wandert. Dies ist insbesondere relevant für temporäre Objekte, kann aber auch in anderen Fällen hilfreich sein. Dies bringt konkret bei std::mt19937 zwar kaum etwas, da die Implementierung ohne Zeiger und ohne dynamisch belegte Datenstrukturen auskommen sollte und somit der Kopieraufwand sich nicht in jedem Falle vermeiden lässt. Da der Pool von dem verwendeten Generator unabhängig ist, sollten wir dennoch das Potential dazu nicht ignorieren.

Die Idee ist, dass wir ein Objekt behandeln können, als ob es ein temporäres Objekt wäre, indem wir es in eine sogenannte rvalue reference konvertieren, d.h. eine Referenz auf ein Objekt, das wir (für das Verschieben) verändern dürfen, da es anschließend nicht mehr benötigt wird. In C++ wird für eine rvalue reference && verwendet. Normalerweise passen entsprechende Parameter nur für temporäre Werte, aber mit Hilfe von std::move kann auch ein nicht-temporärer Wert als temporärer behandelt werden, um die Verschiebesemantik zu ermöglichen. Schauen wir uns das zunächst bei der free-Methode an:

void free(T&& engine) {
   std::lock_guard<std::mutex> lock(mutex);
   unused.push_back(engine);
}

Sie besteht jetzt auf einer rvalue reference, d.h. es muss entsprechend std::move beim Aufruf verwendet werden. Dies ist hier sinnvoll, da das Objekt zurückgegeben werden soll und der Aufrufer das zurückgegebene Objekt anschließend nicht mehr verwenden darf. Die std::list-Klasse hat ebenso eine push_back-Methode, die eine rvalue reference entgegennimmt. Auf diese Weise wird sichergestellt, dass das Objekt insgesamt maximal einmal kopiert wird und nicht doppelt wie zuvor.

Innerhalb der get-Methode können wir std::move ebenfalls einsetzen, um eine Kopie zu vermeiden:

T rg = std::move(unused.front());
unused.pop_front();

Die front-Methode liefert eine lvalue reference zurück (also T&), mit Hilfe von std::move wird daraus eine rvalue reference (also T&&). Entsprechend kann dann das rg-Objekt mit einem Konstruktor erzeugt werden, das eine rvalue reference erwartet (also T(T&& other)) an Stelle eines normalen Kopierkonstruktors (also T(T& other)). Auf diese Weise verbleibt vorne in der linearen Liste u.U. eine temporäre Hülle, die dann aber danach mit pop_front() ohnehin sogleich weggeworfen wird.

Aufgabe

Damit eine entsprechend angepasste Template-Klasse RandomEnginePool gut benutzbar ist, sollte dieser noch eine Klasse RandomEngineGuard mitgegeben werden, die entsprechend dem RAII-Prinzip

Das sollte sich dann so benutzen lassen:

template<typename MA, typename Pool>
typename std::enable_if<hpc::matvec::IsRealGeMatrix<MA>::value, void>::type
randomInit(MA& A, Pool& pool) {
   using ElementType = typename MA::ElementType;
   using Index = typename MA::Index;
   using EngineType = typename Pool::EngineType;

   std::uniform_real_distribution<double> uniform(-100, 100);
   RandomEngineGuard<EngineType> guard(pool);
   auto& engine(guard.get());

   hpc::matvec::apply(A, [&](ElementType& val, Index i, Index j) -> void {
      val = uniform(engine);
   });
}