From 3330c02adad931e8c6a7d522f59126ab4b9fa776 Mon Sep 17 00:00:00 2001 From: Matthias Maier Date: Tue, 24 Oct 2023 11:33:04 -0500 Subject: [PATCH] base: Implement a Lazy wrapper Implement an optimized, thin wrapper that provides a convenient mechanism for lazy initialization of the contained object on first use. The class ensures that on-demand initialization of some expensive data structure happens (a) in a thread-safe manner, and that (b) subsequent checks in hot paths are cheap: - The class implements proper copy and move semantics inherited from std::optional. This is particularly desirable for our typical use case in deal.II where we want to initialize some payload exactly once (and simply copy or move it along with the container). - The "passive" call, i.e., when the object is already initialized is much cheaper than a call to std::call_once, or thread synchronization with a std::shared_mutex (because the initialize() or value_or_initialize() functions are inlined and consist of a mere check of a boolean followed by a conditional jump). --- include/deal.II/base/lazy.h | 378 ++++++++++++++++++++++++++++++++++++ 1 file changed, 378 insertions(+) create mode 100644 include/deal.II/base/lazy.h diff --git a/include/deal.II/base/lazy.h b/include/deal.II/base/lazy.h new file mode 100644 index 0000000000..e2c5f22b06 --- /dev/null +++ b/include/deal.II/base/lazy.h @@ -0,0 +1,378 @@ +// --------------------------------------------------------------------- +// +// Copyright (C) 2023 - 2023 by the deal.II authors +// +// This file is part of the deal.II library. +// +// The deal.II library is free software; you can use it, redistribute +// it, and/or modify it under the terms of the GNU Lesser General +// Public License as published by the Free Software Foundation; either +// version 2.1 of the License, or (at your option) any later version. +// The full text of the license can be found in the file LICENSE.md at +// the top level directory of deal.II. +// +// --------------------------------------------------------------------- + +#ifndef dealii_lazy_h +#define dealii_lazy_h + + +#include + +#include +#include + +#include +#include +#include + +DEAL_II_NAMESPACE_OPEN + +/** + * @addtogroup threads + * @{ + */ + +/** + * This class is a wrapper that provides a convenient mechanism for lazy + * initialization of the contained object on first use. The class ensures + * that on-demand initialization of some expensive data structure happens + * (a) exactly once in a thread-safe manner, and that (b) subsequent checks + * in hot paths are cheap. + * + * Lazy is closely modeled after the `std::optional` interface providing + * a `reset()` and `value()` method, but also and extending it with two + * methods: `ensure_initialized(creator)` which, as the name suggests, + * ensures that the contained object is properly initialized. If the + * `Lazy` happens happens to contain no value yet, it initializes the + * wrapped object by calling the `creator()` function object and storing + * the return value. In addition a `value_or_initialize(creator)` function + * is provided that, similarly, ensures that the object is properly + * initialized and then returns a reference to the contained value. + * + * Example usage could look like the following, where the `FE` class stores a + * matrix that is expensive to compute and so we do not want to do it unless it + * is actually needed. As a consequence, rather than storing a matrix, we store + * a `Lazy>` that by default is empty; whenever the matrix is + * first requested, we create it and store it for later reuse: + * ``` + * template<...> + * class FE + * { + * public: + * const FullMatrix & get_prolongation_matrix() const + * { + * prolongation_matrix.ensure_initialized([&](){ + * // Some expensive operation initializing the prolongation matrix + * // that we only want to perform once and when necessary. + * }); + * return prolongation_matrix.value(); + * } + * + * private: + * Lazy> prolongation_matrix; + * }; + * ``` + */ +template +class Lazy +{ +public: + /** + * Default Constructor. + */ + Lazy(); + + + /** + * Copy constructor. + */ + Lazy(const Lazy &other); + + + /** + * Move constructor. + */ + Lazy(Lazy &&other) noexcept; + + + /** + * Copy assignment. + */ + Lazy & + operator=(const Lazy &other); + + + /** + * Move assignment. + */ + Lazy & + operator=(Lazy &&other) noexcept; + + + /** + * Reset the Lazy object to an uninitialized state. + */ + void + reset() noexcept; + + + /** + * Initialize the wrapped object. + * + * If the contained object is already initialized this function simply + * returns and does nothing. + * + * If, instead, the object has not yet been initialized then the @p + * creator function object (oftentimes a lambda function) is called to + * initialize the contained object. + * + * This operation is thread safe: The ensure_initialized() method + * guarantees that the creator function object is only called once on one + * of the calling threads and that after completion the initialization + * result (which is stored in the std::optional) is visible on all + * threads. + */ + template + void + ensure_initialized(const Callable &creator) const; + + + /** + * Return a const reference to the contained object. + * + * @pre The object has been initialized with a call to + * ensure_initialized() or value_or_initialized(). + */ + const T & + value() const; + + + /** + * Return a reference to the contained object. + * + * @pre The object has been initialized with a call to + * ensure_initialized() or value_or_initialized(). + */ + T & + value(); + + + /** + * If the underlying object is initialized the function simply returns a + * const reference to the contained value. Otherwise, the @p creator() + * function object is called to initialize the object first. + * + * This function mimics the syntax of the std::optional interface and + * is functionally equivalent to calling ensure_initialized() followed by + * value(). + * + * @note This method can be called from a context where the Lazy + * wrapper itself is marked const. FIXME + * + * @post The underlying object is initialized, meaning, has_value() + * returns true. + */ + template + const T & + value_or_initialize(const Callable &creator) const; + + + /** + * Variant of above function that returns a non-const reference. + */ + template + DEAL_II_ALWAYS_INLINE inline T & + value_or_initialize(const Callable &creator); + + +private: + /** + * The lazily initialized object stored as a std::optional. + */ + mutable std::optional object; + + + /** + * An atomic bool used for checking whether the object is initialized in + * a thread-safe manner. + */ + mutable std::atomic object_is_initialized; + + + /** + * A mutex used for protecting the initialization of the object. + */ + mutable Threads::Mutex initialization_mutex; +}; + +/** + * @} + */ + + +// ------------------------------- inline functions -------------------------- + + +template +inline Lazy::Lazy() + : object_is_initialized(false) +{} + + +template +inline Lazy::Lazy(const Lazy &other) + : object(other.object) +{ + object_is_initialized.store(other.object_is_initialized.load()); +} + + +template +inline Lazy::Lazy(Lazy &&other) noexcept + : object(other.object) +{ + object_is_initialized.store(other.object_is_initialized.load()); +} + + +template +inline Lazy & +Lazy::operator=(const Lazy &other) +{ + object = other.object; + object_is_initialized.store(other.object_is_initialized.load()); + return *this; +} + + +template +inline Lazy & +Lazy::operator=(Lazy &&other) noexcept +{ + object = other.object; + object_is_initialized.store(other.object_is_initialized.load()); + return *this; +} + + +template +inline void +Lazy::reset() noexcept +{ + object_is_initialized.store(false); + object.reset(); +} + + +template +template +inline DEAL_II_ALWAYS_INLINE void +Lazy::ensure_initialized(const Callable &creator) const +{ + // + // Use Schmidt's double checking [1] for checking and initializing the + // object. + // + // [1] https://en.wikipedia.org/wiki/Double-checked_locking + // + + // + // Check the object_is_initialized atomic with "acquire" semantics [1]. + // + // This ensures that (a) all subsequent reads (of the object) are + // ordered after this check, and that (b) all writes to the object + // before the atomic bool was set to true with "release" semantics are + // visible on this thread. + // + // [1] + // https://en.cppreference.com/w/cpp/atomic/memory_order#Release-Acquire_ordering + // + if (!object_is_initialized.load(std::memory_order_acquire)) +#ifdef DEAL_II_HAVE_CXX20 + [[unlikely]] +#endif + { + std::lock_guard lock(initialization_mutex); + + // + // Check again. If this thread won the race to the lock then we + // initialize the object. Otherwise another thread has already + // initialized the object and flipped the object_is_initialized + // bit. (Here, the initialization_mutex ensures consistent ordering + // with a memory fence, so we will observe the updated bool without + // acquire semantics.) + // + if (!object_is_initialized.load(std::memory_order_relaxed)) + { + object = std::move(creator()); + + // + // Flip the object_is_initialized boolean with "release" + // semantics [1]. + // + // This ensures that the above move is visible on all threads + // before checking the atomic bool with acquire semantics. + // + object_is_initialized.store(true, std::memory_order_release); + } + } + + Assert( + object.has_value(), + dealii::ExcMessage( + "The internal std::optional object does not contain a valid object " + "even though we have just initialized it.")); +} + + +template +inline DEAL_II_ALWAYS_INLINE const T & +Lazy::value() const +{ + Assert( + object.has_value(), + dealii::ExcMessage( + "value() has been called but the contained object has not been " + "initialized. Did you forget to call 'ensure_initialized()' first?")); + + return object.value(); +} + + +template +inline DEAL_II_ALWAYS_INLINE T & +Lazy::value() +{ + Assert( + object.has_value(), + dealii::ExcMessage( + "value() has been called but the contained object has not been " + "initialized. Did you forget to call 'ensure_initialized()' first?")); + + return object.value(); +} + + +template +template +inline DEAL_II_ALWAYS_INLINE const T & +Lazy::value_or_initialize(const Callable &creator) const +{ + ensure_initialized(creator); + return object.value(); +} + + +template +template +inline DEAL_II_ALWAYS_INLINE T & +Lazy::value_or_initialize(const Callable &creator) +{ + ensure_initialized(creator); + return object.value(); +} + + +DEAL_II_NAMESPACE_CLOSE +#endif -- 2.39.5