| //===- StorageUniquer.cpp - Common Storage Class Uniquer ------------------===// | |
| // | |
| // 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 | |
| // | |
| //===----------------------------------------------------------------------===// | |
| #include "mlir/Support/StorageUniquer.h" | |
| #include "mlir/Support/LLVM.h" | |
| #include "mlir/Support/ThreadLocalCache.h" | |
| #include "mlir/Support/TypeID.h" | |
| #include "llvm/Support/RWMutex.h" | |
| using namespace mlir; | |
| using namespace mlir::detail; | |
| namespace { | |
| /// This class represents a uniquer for storage instances of a specific type | |
| /// that has parametric storage. It contains all of the necessary data to unique | |
| /// storage instances in a thread safe way. This allows for the main uniquer to | |
| /// bucket each of the individual sub-types removing the need to lock the main | |
| /// uniquer itself. | |
| class ParametricStorageUniquer { | |
| public: | |
| using BaseStorage = StorageUniquer::BaseStorage; | |
| using StorageAllocator = StorageUniquer::StorageAllocator; | |
| /// A lookup key for derived instances of storage objects. | |
| struct LookupKey { | |
| /// The known hash value of the key. | |
| unsigned hashValue; | |
| /// An equality function for comparing with an existing storage instance. | |
| function_ref<bool(const BaseStorage *)> isEqual; | |
| }; | |
| private: | |
| /// A utility wrapper object representing a hashed storage object. This class | |
| /// contains a storage object and an existing computed hash value. | |
| struct HashedStorage { | |
| HashedStorage(unsigned hashValue = 0, BaseStorage *storage = nullptr) | |
| : hashValue(hashValue), storage(storage) {} | |
| unsigned hashValue; | |
| BaseStorage *storage; | |
| }; | |
| /// Storage info for derived TypeStorage objects. | |
| struct StorageKeyInfo { | |
| static inline unsigned getHashValue(const HashedStorage &key) { | |
| return key.hashValue; | |
| } | |
| static inline unsigned getHashValue(const LookupKey &key) { | |
| return key.hashValue; | |
| } | |
| static inline bool isEqual(const HashedStorage &lhs, | |
| const HashedStorage &rhs) { | |
| return lhs.storage == rhs.storage; | |
| } | |
| static inline bool isEqual(const LookupKey &lhs, const HashedStorage &rhs) { | |
| // Invoke the equality function on the lookup key. | |
| return lhs.isEqual(rhs.storage); | |
| } | |
| }; | |
| using StorageTypeSet = DenseSet<HashedStorage, StorageKeyInfo>; | |
| /// This class represents a single shard of the uniquer. The uniquer uses a | |
| /// set of shards to allow for multiple threads to create instances with less | |
| /// lock contention. | |
| struct Shard { | |
| /// The set containing the allocated storage instances in the base layer. | |
| StorageTypeSet instances; | |
| /// The set containing the allocated storage instances in the transient | |
| /// layer, lazily instantiated on the first transient allocation. | |
| std::unique_ptr<StorageTypeSet> transientInstances; | |
| #if LLVM_ENABLE_THREADS != 0 | |
| /// A mutex to keep uniquing thread-safe. | |
| llvm::sys::SmartRWMutex<true> mutex; | |
| #endif | |
| }; | |
| /// Get or create an instance of a param derived type in an thread-unsafe | |
| /// fashion. | |
| BaseStorage *getOrCreateUnsafe(Shard &shard, LookupKey &key, | |
| function_ref<BaseStorage *()> ctorFn) { | |
| if (isInTransientScope()) { | |
| // If we are in a transient scope, then it means we had a base context | |
| // which was frozen at some point and are busy mutating in a transient | |
| // overlay state. Check to see if the instance is in either of these | |
| // first before creating. The expectation is that the base state is | |
| // rather minimal and most of the entries are in the transient, so | |
| // search in transient instances first before base, and if neither | |
| // create a transient instance. | |
| if (shard.transientInstances) { | |
| auto transientIt = shard.transientInstances->find_as(key); | |
| if (transientIt != shard.transientInstances->end()) | |
| return transientIt->storage; | |
| } | |
| auto baseIt = shard.instances.find_as(key); | |
| if (baseIt != shard.instances.end()) | |
| return baseIt->storage; | |
| if (!shard.transientInstances) | |
| shard.transientInstances = std::make_unique<StorageTypeSet>(); | |
| auto existing = shard.transientInstances->insert_as({key.hashValue}, key); | |
| BaseStorage *&storage = existing.first->storage; | |
| if (existing.second) | |
| storage = ctorFn(); | |
| return storage; | |
| } | |
| auto existing = shard.instances.insert_as({key.hashValue}, key); | |
| BaseStorage *&storage = existing.first->storage; | |
| if (existing.second) | |
| storage = ctorFn(); | |
| return storage; | |
| } | |
| /// Destroy all of the storage instances within the given shard. | |
| void destroyShardInstances(Shard &shard) { | |
| if (!destructorFn) | |
| return; | |
| for (HashedStorage &instance : shard.instances) | |
| destructorFn(instance.storage); | |
| if (shard.transientInstances) { | |
| for (HashedStorage &instance : *shard.transientInstances) | |
| destructorFn(instance.storage); | |
| } | |
| } | |
| public: | |
| #if LLVM_ENABLE_THREADS != 0 | |
| /// Initialize the storage uniquer with a given number of storage shards to | |
| /// use. The provided shard number is required to be a valid power of 2. The | |
| /// destructor function is used to destroy any allocated storage instances. | |
| ParametricStorageUniquer(function_ref<void(BaseStorage *)> destructorFn, | |
| size_t numShards = 8) | |
| : shards(new std::atomic<Shard *>[numShards]), | |
| destructorFn(destructorFn) { | |
| assert(llvm::isPowerOf2_64(numShards) && | |
| "the number of shards is required to be a power of 2"); | |
| this->numShards = numShards; | |
| this->inTransientScope = false; | |
| for (size_t i = 0; i < numShards; i++) | |
| shards[i].store(nullptr, std::memory_order_relaxed); | |
| } | |
| ~ParametricStorageUniquer() { | |
| // Free all of the allocated shards. | |
| for (size_t i = 0; i != numShards; ++i) { | |
| if (Shard *shard = shards[i].load()) { | |
| destroyShardInstances(*shard); | |
| delete shard; | |
| } | |
| } | |
| } | |
| /// Get or create an instance of a parametric type. | |
| BaseStorage *getOrCreate(bool threadingIsEnabled, unsigned hashValue, | |
| function_ref<bool(const BaseStorage *)> isEqual, | |
| function_ref<BaseStorage *()> ctorFn) { | |
| Shard &shard = getShard(hashValue); | |
| ParametricStorageUniquer::LookupKey lookupKey{hashValue, isEqual}; | |
| if (!threadingIsEnabled) | |
| return getOrCreateUnsafe(shard, lookupKey, ctorFn); | |
| // Check for a instance of this object in the local cache. | |
| auto localIt = localCache->insert_as({hashValue}, lookupKey); | |
| BaseStorage *&localInst = localIt.first->storage; | |
| if (localInst) | |
| return localInst; | |
| // Check for an existing instance in read-only mode. | |
| { | |
| llvm::sys::SmartScopedReader<true> typeLock(shard.mutex); | |
| if (isInTransientScope() && shard.transientInstances) { | |
| auto it = shard.transientInstances->find_as(lookupKey); | |
| if (it != shard.transientInstances->end()) | |
| return localInst = it->storage; | |
| } | |
| auto it = shard.instances.find_as(lookupKey); | |
| if (it != shard.instances.end()) | |
| return localInst = it->storage; | |
| } | |
| // Acquire a writer-lock so that we can safely create the new storage | |
| // instance. | |
| llvm::sys::SmartScopedWriter<true> typeLock(shard.mutex); | |
| return localInst = getOrCreateUnsafe(shard, lookupKey, ctorFn); | |
| } | |
| /// Run a mutation function on the provided storage object in a thread-safe | |
| /// way. | |
| LogicalResult mutate(bool threadingIsEnabled, BaseStorage *storage, | |
| function_ref<LogicalResult()> mutationFn) { | |
| if (!threadingIsEnabled) | |
| return mutationFn(); | |
| // Get a shard to use for mutating this storage instance. It doesn't need to | |
| // be the same shard as the original allocation, but does need to be | |
| // deterministic. | |
| Shard &shard = getShard(llvm::hash_value(storage)); | |
| llvm::sys::SmartScopedWriter<true> lock(shard.mutex); | |
| return mutationFn(); | |
| } | |
| void beginTransientScope() { | |
| assert(!isInTransientScope() && | |
| "parametric storage uniquer is already in a transient scope"); | |
| inTransientScope = true; | |
| } | |
| void endTransientScope() { | |
| assert(isInTransientScope() && | |
| "parametric storage uniquer is not in a transient scope"); | |
| if (!isInTransientScope()) | |
| return; | |
| for (size_t i = 0; i != numShards; ++i) { | |
| if (Shard *shard = shards[i].load()) { | |
| llvm::sys::SmartScopedWriter<true> typeLock(shard->mutex); | |
| if (shard->transientInstances) { | |
| if (destructorFn) { | |
| for (HashedStorage &instance : *shard->transientInstances) | |
| destructorFn(instance.storage); | |
| } | |
| shard->transientInstances.reset(); | |
| } | |
| } | |
| } | |
| localCache.clear(); | |
| inTransientScope = false; | |
| } | |
| bool isInTransientScope() const { return inTransientScope; } | |
| size_t getNumShards() const { return numShards; } | |
| private: | |
| /// Return the shard used for the given hash value. | |
| Shard &getShard(unsigned hashValue) { | |
| // Get a shard number from the provided hashvalue. | |
| unsigned shardNum = hashValue & (numShards - 1); | |
| // Try to acquire an already initialized shard. | |
| Shard *shard = shards[shardNum].load(std::memory_order_acquire); | |
| if (shard) | |
| return *shard; | |
| // Otherwise, try to allocate a new shard. | |
| Shard *newShard = new Shard(); | |
| if (shards[shardNum].compare_exchange_strong(shard, newShard)) | |
| return *newShard; | |
| // If one was allocated before we can initialize ours, delete ours. | |
| delete newShard; | |
| return *shard; | |
| } | |
| /// A thread local cache for storage objects. This helps to reduce the lock | |
| /// contention when an object already existing in the cache. | |
| ThreadLocalCache<StorageTypeSet> localCache; | |
| /// A set of uniquer shards to allow for further bucketing accesses for | |
| /// instances of this storage type. Each shard is lazily initialized to reduce | |
| /// the overhead when only a small amount of shards are in use. | |
| std::unique_ptr<std::atomic<Shard *>[]> shards; | |
| /// The number of available shards. | |
| unsigned numShards : 31; | |
| /// Whether the uniquer is currently in a transient scope. | |
| unsigned inTransientScope : 1; | |
| /// Function to used to destruct any allocated storage instances. | |
| function_ref<void(BaseStorage *)> destructorFn; | |
| #else | |
| /// If multi-threading is disabled, ignore the shard parameter as we will | |
| /// always use one shard. The destructor function is used to destroy any | |
| /// allocated storage instances. | |
| ParametricStorageUniquer(function_ref<void(BaseStorage *)> destructorFn, | |
| size_t numShards = 0) | |
| : destructorFn(destructorFn) {} | |
| ~ParametricStorageUniquer() { destroyShardInstances(shard); } | |
| /// Get or create an instance of a parametric type. | |
| BaseStorage * | |
| getOrCreate(bool threadingIsEnabled, unsigned hashValue, | |
| function_ref<bool(const BaseStorage *)> isEqual, | |
| function_ref<BaseStorage *()> ctorFn) { | |
| ParametricStorageUniquer::LookupKey lookupKey{hashValue, isEqual}; | |
| return getOrCreateUnsafe(shard, lookupKey, ctorFn); | |
| } | |
| /// Run a mutation function on the provided storage object in a thread-safe | |
| /// way. | |
| LogicalResult | |
| mutate(bool threadingIsEnabled, BaseStorage *storage, | |
| function_ref<LogicalResult()> mutationFn) { | |
| return mutationFn(); | |
| } | |
| void beginTransientScope() { | |
| assert(!inTransientScope && | |
| "parametric storage uniquer is already in a transient scope"); | |
| inTransientScope = true; | |
| } | |
| void endTransientScope() { | |
| assert(inTransientScope && | |
| "parametric storage uniquer is not in a transient scope"); | |
| if (!inTransientScope) | |
| return; | |
| if (shard.transientInstances) { | |
| if (destructorFn) { | |
| for (HashedStorage &instance : *shard.transientInstances) | |
| destructorFn(instance.storage); | |
| } | |
| shard.transientInstances.reset(); | |
| } | |
| inTransientScope = false; | |
| } | |
| bool isInTransientScope() const { return inTransientScope; } | |
| private: | |
| /// The main uniquer shard that is used for allocating storage instances. | |
| Shard shard; | |
| /// Function to used to destruct any allocated storage instances. | |
| function_ref<void(BaseStorage *)> destructorFn; | |
| /// Flag indicating if the uniquer is currently in a transient scope. | |
| bool inTransientScope = false; | |
| #endif | |
| }; | |
| } // namespace | |
| namespace mlir { | |
| namespace detail { | |
| /// This is the implementation of the StorageUniquer class. | |
| struct StorageUniquerImpl { | |
| using BaseStorage = StorageUniquer::BaseStorage; | |
| using StorageAllocator = StorageUniquer::StorageAllocator; | |
| /// Bundled state dynamically allocated when entering a transient scope. | |
| struct TransientState { | |
| #if LLVM_ENABLE_THREADS != 0 | |
| /// Transient thread local set of allocators used when in a transient scope. | |
| ThreadLocalCache<StorageAllocator *> threadSafeAllocator; | |
| /// Transient allocators created during transient scope. | |
| std::vector<std::unique_ptr<StorageAllocator>> threadAllocators; | |
| /// A mutex used for safely adding a new transient thread allocator. | |
| llvm::sys::SmartMutex<true> threadAllocatorMutex; | |
| #endif | |
| /// Single-threaded allocator used during transient scope. | |
| std::unique_ptr<StorageAllocator> allocator; | |
| /// Transient singleton instances registered during transient scope. | |
| DenseMap<TypeID, BaseStorage *> singletonInstances; | |
| }; | |
| //===--------------------------------------------------------------------===// | |
| // Parametric Storage | |
| //===--------------------------------------------------------------------===// | |
| /// Check if an instance of a parametric storage class exists. | |
| bool hasParametricStorage(TypeID id) { return parametricUniquers.count(id); } | |
| /// Get or create an instance of a parametric type. | |
| BaseStorage * | |
| getOrCreate(TypeID id, unsigned hashValue, | |
| function_ref<bool(const BaseStorage *)> isEqual, | |
| function_ref<BaseStorage *(StorageAllocator &)> ctorFn) { | |
| assert(parametricUniquers.count(id) && | |
| "creating unregistered storage instance"); | |
| ParametricStorageUniquer &storageUniquer = *parametricUniquers[id]; | |
| return storageUniquer.getOrCreate( | |
| threadingIsEnabled, hashValue, isEqual, | |
| [&] { return ctorFn(getThreadSafeAllocator()); }); | |
| } | |
| /// Run a mutation function on the provided storage object in a thread-safe | |
| /// way. | |
| LogicalResult | |
| mutate(TypeID id, BaseStorage *storage, | |
| function_ref<LogicalResult(StorageAllocator &)> mutationFn) { | |
| assert(parametricUniquers.count(id) && | |
| "mutating unregistered storage instance"); | |
| ParametricStorageUniquer &storageUniquer = *parametricUniquers[id]; | |
| return storageUniquer.mutate(threadingIsEnabled, storage, [&] { | |
| return mutationFn(getThreadSafeAllocator()); | |
| }); | |
| } | |
| /// Return an allocator that can be used to safely allocate instances on the | |
| /// current thread. | |
| StorageAllocator &getThreadSafeAllocator() { | |
| #if LLVM_ENABLE_THREADS != 0 | |
| if (!threadingIsEnabled) { | |
| if (transientState) { | |
| if (!transientState->allocator) | |
| transientState->allocator = std::make_unique<StorageAllocator>(); | |
| return *transientState->allocator; | |
| } | |
| return allocator; | |
| } | |
| if (transientState) { | |
| StorageAllocator *&threadAllocator = | |
| transientState->threadSafeAllocator.get(); | |
| if (!threadAllocator) { | |
| threadAllocator = new StorageAllocator(); | |
| llvm::sys::SmartScopedLock<true> lock( | |
| transientState->threadAllocatorMutex); | |
| transientState->threadAllocators.push_back( | |
| std::unique_ptr<StorageAllocator>(threadAllocator)); | |
| } | |
| return *threadAllocator; | |
| } | |
| // If the allocator has not been initialized, create a new one. | |
| StorageAllocator *&threadAllocator = threadSafeAllocator.get(); | |
| if (!threadAllocator) { | |
| threadAllocator = new StorageAllocator(); | |
| // Record this allocator, given that we don't want it to be destroyed when | |
| // the thread dies. | |
| llvm::sys::SmartScopedLock<true> lock(threadAllocatorMutex); | |
| threadAllocators.push_back( | |
| std::unique_ptr<StorageAllocator>(threadAllocator)); | |
| } | |
| return *threadAllocator; | |
| #else | |
| if (transientState) { | |
| if (!transientState->allocator) | |
| transientState->allocator = std::make_unique<StorageAllocator>(); | |
| return *transientState->allocator; | |
| } | |
| return allocator; | |
| #endif | |
| } | |
| void beginTransientScope() { | |
| assert(!transientState && | |
| "storage uniquer is already in a transient scope"); | |
| transientState = std::make_unique<TransientState>(); | |
| for (auto &entry : parametricUniquers) | |
| entry.second->beginTransientScope(); | |
| } | |
| void endTransientScope() { | |
| assert(transientState && "storage uniquer is not in a transient scope"); | |
| if (!transientState) | |
| return; | |
| for (auto &entry : parametricUniquers) | |
| entry.second->endTransientScope(); | |
| transientState.reset(); | |
| } | |
| bool isInTransientScope() const { return transientState != nullptr; } | |
| //===--------------------------------------------------------------------===// | |
| // Singleton Storage | |
| //===--------------------------------------------------------------------===// | |
| /// Get or create an instance of a singleton storage class. | |
| BaseStorage *getSingleton(TypeID id) { | |
| if (transientState) { | |
| auto it = transientState->singletonInstances.find(id); | |
| if (it != transientState->singletonInstances.end()) | |
| return it->second; | |
| } | |
| BaseStorage *singletonInstance = singletonInstances[id]; | |
| assert(singletonInstance && "expected singleton instance to exist"); | |
| return singletonInstance; | |
| } | |
| /// Check if an instance of a singleton storage class exists. | |
| bool hasSingleton(TypeID id) const { | |
| if (transientState && transientState->singletonInstances.count(id)) | |
| return true; | |
| return singletonInstances.count(id); | |
| } | |
| //===--------------------------------------------------------------------===// | |
| // Instance Storage | |
| //===--------------------------------------------------------------------===// | |
| #if LLVM_ENABLE_THREADS != 0 | |
| /// A thread local set of allocators used for uniquing parametric instances, | |
| /// or other data allocated in thread volatile situations. | |
| ThreadLocalCache<StorageAllocator *> threadSafeAllocator; | |
| /// All of the allocators that have been created for thread based allocation. | |
| std::vector<std::unique_ptr<StorageAllocator>> threadAllocators; | |
| /// A mutex used for safely adding a new thread allocator. | |
| llvm::sys::SmartMutex<true> threadAllocatorMutex; | |
| #endif | |
| /// Main allocator used for uniquing singleton instances, and other state when | |
| /// thread safety is guaranteed. | |
| StorageAllocator allocator; | |
| /// Transient state bundled into a unique pointer (nullptr when inactive). | |
| std::unique_ptr<TransientState> transientState; | |
| /// Map of type ids to the storage uniquer to use for registered objects. | |
| DenseMap<TypeID, std::unique_ptr<ParametricStorageUniquer>> | |
| parametricUniquers; | |
| /// Map of type ids to a singleton instance when the storage class is a | |
| /// singleton. | |
| DenseMap<TypeID, BaseStorage *> singletonInstances; | |
| /// Flag specifying if multi-threading is enabled within the uniquer. | |
| bool threadingIsEnabled = true; | |
| }; | |
| } // namespace detail | |
| } // namespace mlir | |
| StorageUniquer::StorageUniquer() : impl(new StorageUniquerImpl()) {} | |
| StorageUniquer::~StorageUniquer() = default; | |
| /// Set the flag specifying if multi-threading is disabled within the uniquer. | |
| void StorageUniquer::disableMultithreading(bool disable) { | |
| impl->threadingIsEnabled = !disable; | |
| } | |
| void StorageUniquer::beginTransientScope() { impl->beginTransientScope(); } | |
| void StorageUniquer::endTransientScope() { impl->endTransientScope(); } | |
| bool StorageUniquer::isInTransientScope() const { | |
| return impl->isInTransientScope(); | |
| } | |
| /// Implementation for getting/creating an instance of a derived type with | |
| /// parametric storage. | |
| auto StorageUniquer::getParametricStorageTypeImpl( | |
| TypeID id, unsigned hashValue, | |
| function_ref<bool(const BaseStorage *)> isEqual, | |
| function_ref<BaseStorage *(StorageAllocator &)> ctorFn) -> BaseStorage * { | |
| return impl->getOrCreate(id, hashValue, isEqual, ctorFn); | |
| } | |
| /// Implementation for registering an instance of a derived type with | |
| /// parametric storage. | |
| void StorageUniquer::registerParametricStorageTypeImpl( | |
| TypeID id, function_ref<void(BaseStorage *)> destructorFn) { | |
| auto uniquer = std::make_unique<ParametricStorageUniquer>(destructorFn); | |
| if (impl->isInTransientScope()) | |
| uniquer->beginTransientScope(); | |
| impl->parametricUniquers.try_emplace(id, std::move(uniquer)); | |
| } | |
| /// Implementation for getting an instance of a derived type with default | |
| /// storage. | |
| auto StorageUniquer::getSingletonImpl(TypeID id) -> BaseStorage * { | |
| return impl->getSingleton(id); | |
| } | |
| /// Test is the storage singleton is initialized. | |
| bool StorageUniquer::isSingletonStorageInitialized(TypeID id) { | |
| return impl->hasSingleton(id); | |
| } | |
| /// Test is the parametric storage is initialized. | |
| bool StorageUniquer::isParametricStorageInitialized(TypeID id) { | |
| return impl->hasParametricStorage(id); | |
| } | |
| /// Implementation for registering an instance of a derived type with default | |
| /// storage. | |
| void StorageUniquer::registerSingletonImpl( | |
| TypeID id, function_ref<BaseStorage *(StorageAllocator &)> ctorFn) { | |
| if (impl->transientState) { | |
| assert(!impl->transientState->singletonInstances.count(id) && | |
| !impl->singletonInstances.count(id) && | |
| "storage class already registered"); | |
| impl->transientState->singletonInstances.try_emplace( | |
| id, ctorFn(impl->getThreadSafeAllocator())); | |
| return; | |
| } | |
| assert(!impl->singletonInstances.count(id) && | |
| "storage class already registered"); | |
| impl->singletonInstances.try_emplace(id, ctorFn(impl->allocator)); | |
| } | |
| /// Implementation for mutating an instance of a derived storage. | |
| LogicalResult StorageUniquer::mutateImpl( | |
| TypeID id, BaseStorage *storage, | |
| function_ref<LogicalResult(StorageAllocator &)> mutationFn) { | |
| return impl->mutate(id, storage, mutationFn); | |
| } |