======================= 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: ---- CODE (type=cpp) ---------------------------------------------------------- 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: ---- CODE (type=cpp) ---------------------------------------------------------- void free(T engine) { std::lock_guard 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` 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: ---- CODE (type=cpp) ---------------------------------------------------------- void free(T&& engine) { std::lock_guard 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: ---- CODE (type=cpp) ---------------------------------------------------------- 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 * beim Initialisieren sich einen Generator aus dem Pool ausleiht, * diesen mit einer Methode `get` als _lvalue reference_ zur Verfügung stellt und * beim Abbau den Generator wieder an den Pool zurückgibt. Das sollte sich dann so benutzen lassen: ---- CODE (type=cpp) ---------------------------------------------------------- template typename std::enable_if::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 uniform(-100, 100); RandomEngineGuard guard(pool); auto& engine(guard.get()); hpc::matvec::apply(A, [&](ElementType& val, Index i, Index j) -> void { val = uniform(engine); }); } ------------------------------------------------------------------------------- :navigate: up -> doc:index back -> doc:session15/page04 next -> doc:session15/page06