//===- ThreadLocalCache.h - ThreadLocalCache class --------------*- C++ -*-===// // // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. // See https://llvm.org/LICENSE.txt for license information. // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception // //===----------------------------------------------------------------------===// // // This file contains a definition of the ThreadLocalCache class. This class // provides support for defining thread local objects with non-static duration. // //===----------------------------------------------------------------------===// #ifndef MLIR_SUPPORT_THREADLOCALCACHE_H #define MLIR_SUPPORT_THREADLOCALCACHE_H #include "mlir/Support/LLVM.h" #include "llvm/ADT/DenseMap.h" #include "llvm/Support/ManagedStatic.h" #include "llvm/Support/Mutex.h" namespace mlir { /// This class provides support for defining a thread local object with non /// static storage duration. This is very useful for situations in which a data /// cache has very large lock contention. template class ThreadLocalCache { struct PerInstanceState; using PointerAndFlag = std::pair>; /// The "observer" is owned by a thread-local cache instance. It is /// constructed the first time a `ThreadLocalCache` instance is accessed by a /// thread, unless `perInstanceState` happens to get re-allocated to the same /// address as a previous one. A `thread_local` instance of this class is /// destructed when the thread in which it lives is destroyed. /// /// This class is called the "observer" because while values cached in /// thread-local caches are owned by `PerInstanceState`, a reference is stored /// via this class in the TLC. With a double pointer, it knows when the /// referenced value has been destroyed. struct Observer { /// This is the double pointer, explicitly allocated because we need to keep /// the address stable if the TLC map re-allocates. It is owned by the /// observer and shared with the value owner. std::shared_ptr ptr = std::make_shared(std::make_pair(nullptr, false)); /// Because the `Owner` instance that lives inside `PerInstanceState` /// contains a reference to the double pointer, and likewise this class /// contains a reference to the value, we need to synchronize destruction of /// the TLC and the `PerInstanceState` to avoid racing. This weak pointer is /// acquired during TLC destruction if the `PerInstanceState` hasn't entered /// its destructor yet, and prevents it from happening. std::weak_ptr keepalive; }; /// This struct owns the cache entries. It contains a reference back to the /// reference inside the cache so that it can be written to null to indicate /// that the cache entry is invalidated. It needs to do this because /// `perInstanceState` could get re-allocated to the same pointer and we don't /// remove entries from the TLC when it is deallocated. Thus, we have to reset /// the TLC entries to a starting state in case the `ThreadLocalCache` lives /// shorter than the threads. struct Owner { /// Save a pointer to the reference and write it to the newly created entry. Owner(Observer &observer) : value(std::make_unique()), ptrRef(observer.ptr) { observer.ptr->second = true; observer.ptr->first = value.get(); } ~Owner() { if (std::shared_ptr ptr = ptrRef.lock()) { ptr->first = nullptr; ptr->second = false; } } Owner(Owner &&) = default; Owner &operator=(Owner &&) = default; std::unique_ptr value; std::weak_ptr ptrRef; }; // Keep a separate shared_ptr protected state that can be acquired atomically // instead of using shared_ptr's for each value. This avoids a problem // where the instance shared_ptr is locked() successfully, and then the // ThreadLocalCache gets destroyed before remove() can be called successfully. struct PerInstanceState { /// Remove the given value entry. This is called when a thread local cache /// is destructing but still contains references to values owned by the /// `PerInstanceState`. Removal is required because it prevents writeback to /// a pointer that was deallocated. void remove(ValueT *value) { // Erase the found value directly, because it is guaranteed to be in the // list. llvm::sys::SmartScopedLock threadInstanceLock(instanceMutex); auto it = llvm::find_if(instances, [&](Owner &instance) { return instance.value.get() == value; }); assert(it != instances.end() && "expected value to exist in cache"); instances.erase(it); } /// Owning pointers to all of the values that have been constructed for this /// object in the static cache. SmallVector instances; /// A mutex used when a new thread instance has been added to the cache for /// this object. llvm::sys::SmartMutex instanceMutex; }; /// The type used for the static thread_local cache. This is a map between an /// instance of the non-static cache and a weak reference to an instance of /// ValueT. We use a weak reference here so that the object can be destroyed /// without needing to lock access to the cache itself. struct CacheType : public llvm::SmallDenseMap { ~CacheType() { // Remove the values of this cache that haven't already expired. This is // required because if we don't remove them, they will contain a reference // back to the data here that is being destroyed. for (auto &[instance, observer] : *this) if (std::shared_ptr state = observer.keepalive.lock()) state->remove(observer.ptr->first); } /// Clear out any unused entries within the map. This method is not /// thread-safe, and should only be called by the same thread as the cache. void clearExpiredEntries() { this->remove_if( [](const auto &entry) { return !entry.second.ptr->second; }); } }; public: ThreadLocalCache() = default; ~ThreadLocalCache() { // No cleanup is necessary here as the shared_pointer memory will go out of // scope and invalidate the weak pointers held by the thread_local caches. } /// Return an instance of the value type for the current thread. ValueT &get() { // Check for an already existing instance for this thread. CacheType &staticCache = getStaticCache(); Observer &threadInstance = staticCache[perInstanceState.get()]; if (ValueT *value = threadInstance.ptr->first) return *value; // Otherwise, create a new instance for this thread. { llvm::sys::SmartScopedLock threadInstanceLock( perInstanceState->instanceMutex); perInstanceState->instances.emplace_back(threadInstance); } threadInstance.keepalive = perInstanceState; // Before returning the new instance, take the chance to clear out any used // entries in the static map. The cache is only cleared within the same // thread to remove the need to lock the cache itself. // // `threadInstance` aliases a bucket in `staticCache`; clearExpiredEntries // may erase from the map and invalidate that reference. The value itself // lives in heap-stable storage reached through the shared `Observer::ptr` // (not in the bucket), so load it out before clearing. ValueT &value = *threadInstance.ptr->first; staticCache.clearExpiredEntries(); return value; } ValueT &operator*() { return get(); } ValueT *operator->() { return &get(); } private: ThreadLocalCache(ThreadLocalCache &&) = delete; ThreadLocalCache(const ThreadLocalCache &) = delete; ThreadLocalCache &operator=(const ThreadLocalCache &) = delete; /// Return the static thread local instance of the cache type. static CacheType &getStaticCache() { static LLVM_THREAD_LOCAL CacheType cache; return cache; } std::shared_ptr perInstanceState = std::make_shared(); }; } // namespace mlir #endif // MLIR_SUPPORT_THREADLOCALCACHE_H