1 //===- StorageUniquer.cpp - Common Storage Class Uniquer ------------------===// 2 // 3 // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. 4 // See https://llvm.org/LICENSE.txt for license information. 5 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception 6 // 7 //===----------------------------------------------------------------------===// 8 9 #include "mlir/Support/StorageUniquer.h" 10 11 #include "mlir/Support/LLVM.h" 12 #include "mlir/Support/ThreadLocalCache.h" 13 #include "mlir/Support/TypeID.h" 14 #include "llvm/Support/RWMutex.h" 15 16 using namespace mlir; 17 using namespace mlir::detail; 18 19 namespace { 20 /// This class represents a uniquer for storage instances of a specific type 21 /// that has parametric storage. It contains all of the necessary data to unique 22 /// storage instances in a thread safe way. This allows for the main uniquer to 23 /// bucket each of the individual sub-types removing the need to lock the main 24 /// uniquer itself. 25 class ParametricStorageUniquer { 26 public: 27 using BaseStorage = StorageUniquer::BaseStorage; 28 using StorageAllocator = StorageUniquer::StorageAllocator; 29 30 /// A lookup key for derived instances of storage objects. 31 struct LookupKey { 32 /// The known hash value of the key. 33 unsigned hashValue; 34 35 /// An equality function for comparing with an existing storage instance. 36 function_ref<bool(const BaseStorage *)> isEqual; 37 }; 38 39 private: 40 /// A utility wrapper object representing a hashed storage object. This class 41 /// contains a storage object and an existing computed hash value. 42 struct HashedStorage { 43 HashedStorage(unsigned hashValue = 0, BaseStorage *storage = nullptr) 44 : hashValue(hashValue), storage(storage) {} 45 unsigned hashValue; 46 BaseStorage *storage; 47 }; 48 49 /// Storage info for derived TypeStorage objects. 50 struct StorageKeyInfo : DenseMapInfo<HashedStorage> { 51 static HashedStorage getEmptyKey() { 52 return HashedStorage(0, DenseMapInfo<BaseStorage *>::getEmptyKey()); 53 } 54 static HashedStorage getTombstoneKey() { 55 return HashedStorage(0, DenseMapInfo<BaseStorage *>::getTombstoneKey()); 56 } 57 58 static unsigned getHashValue(const HashedStorage &key) { 59 return key.hashValue; 60 } 61 static unsigned getHashValue(LookupKey key) { return key.hashValue; } 62 63 static bool isEqual(const HashedStorage &lhs, const HashedStorage &rhs) { 64 return lhs.storage == rhs.storage; 65 } 66 static bool isEqual(const LookupKey &lhs, const HashedStorage &rhs) { 67 if (isEqual(rhs, getEmptyKey()) || isEqual(rhs, getTombstoneKey())) 68 return false; 69 // Invoke the equality function on the lookup key. 70 return lhs.isEqual(rhs.storage); 71 } 72 }; 73 using StorageTypeSet = DenseSet<HashedStorage, StorageKeyInfo>; 74 75 /// This class represents a single shard of the uniquer. The uniquer uses a 76 /// set of shards to allow for multiple threads to create instances with less 77 /// lock contention. 78 struct Shard { 79 /// The set containing the allocated storage instances. 80 StorageTypeSet instances; 81 82 /// Allocator to use when constructing derived instances. 83 StorageAllocator allocator; 84 85 #if LLVM_ENABLE_THREADS != 0 86 /// A mutex to keep uniquing thread-safe. 87 llvm::sys::SmartRWMutex<true> mutex; 88 #endif 89 }; 90 91 /// Get or create an instance of a param derived type in an thread-unsafe 92 /// fashion. 93 BaseStorage * 94 getOrCreateUnsafe(Shard &shard, LookupKey &key, 95 function_ref<BaseStorage *(StorageAllocator &)> ctorFn) { 96 auto existing = shard.instances.insert_as({key.hashValue}, key); 97 BaseStorage *&storage = existing.first->storage; 98 if (existing.second) 99 storage = ctorFn(shard.allocator); 100 return storage; 101 } 102 103 public: 104 #if LLVM_ENABLE_THREADS != 0 105 /// Initialize the storage uniquer with a given number of storage shards to 106 /// use. The provided shard number is required to be a valid power of 2. 107 ParametricStorageUniquer(size_t numShards = 8) 108 : shards(new std::atomic<Shard *>[numShards]), numShards(numShards) { 109 assert(llvm::isPowerOf2_64(numShards) && 110 "the number of shards is required to be a power of 2"); 111 for (size_t i = 0; i < numShards; i++) 112 shards[i].store(nullptr, std::memory_order_relaxed); 113 } 114 ~ParametricStorageUniquer() { 115 // Free all of the allocated shards. 116 for (size_t i = 0; i != numShards; ++i) 117 if (Shard *shard = shards[i].load()) 118 delete shard; 119 } 120 /// Get or create an instance of a parametric type. 121 BaseStorage * 122 getOrCreate(bool threadingIsEnabled, unsigned hashValue, 123 function_ref<bool(const BaseStorage *)> isEqual, 124 function_ref<BaseStorage *(StorageAllocator &)> ctorFn) { 125 Shard &shard = getShard(hashValue); 126 ParametricStorageUniquer::LookupKey lookupKey{hashValue, isEqual}; 127 if (!threadingIsEnabled) 128 return getOrCreateUnsafe(shard, lookupKey, ctorFn); 129 130 // Check for a instance of this object in the local cache. 131 auto localIt = localCache->insert_as({hashValue}, lookupKey); 132 BaseStorage *&localInst = localIt.first->storage; 133 if (localInst) 134 return localInst; 135 136 // Check for an existing instance in read-only mode. 137 { 138 llvm::sys::SmartScopedReader<true> typeLock(shard.mutex); 139 auto it = shard.instances.find_as(lookupKey); 140 if (it != shard.instances.end()) 141 return localInst = it->storage; 142 } 143 144 // Acquire a writer-lock so that we can safely create the new storage 145 // instance. 146 llvm::sys::SmartScopedWriter<true> typeLock(shard.mutex); 147 return localInst = getOrCreateUnsafe(shard, lookupKey, ctorFn); 148 } 149 /// Run a mutation function on the provided storage object in a thread-safe 150 /// way. 151 LogicalResult 152 mutate(bool threadingIsEnabled, BaseStorage *storage, 153 function_ref<LogicalResult(StorageAllocator &)> mutationFn) { 154 Shard &shard = getShardFor(storage); 155 if (!threadingIsEnabled) 156 return mutationFn(shard.allocator); 157 158 llvm::sys::SmartScopedWriter<true> lock(shard.mutex); 159 return mutationFn(shard.allocator); 160 } 161 162 private: 163 /// Return the shard used for the given hash value. 164 Shard &getShard(unsigned hashValue) { 165 // Get a shard number from the provided hashvalue. 166 unsigned shardNum = hashValue & (numShards - 1); 167 168 // Try to acquire an already initialized shard. 169 Shard *shard = shards[shardNum].load(std::memory_order_acquire); 170 if (shard) 171 return *shard; 172 173 // Otherwise, try to allocate a new shard. 174 Shard *newShard = new Shard(); 175 if (shards[shardNum].compare_exchange_strong(shard, newShard)) 176 return *newShard; 177 178 // If one was allocated before we can initialize ours, delete ours. 179 delete newShard; 180 return *shard; 181 } 182 183 /// Return the shard that allocated the provided storage object. 184 Shard &getShardFor(BaseStorage *storage) { 185 for (size_t i = 0; i != numShards; ++i) { 186 if (Shard *shard = shards[i].load(std::memory_order_acquire)) { 187 llvm::sys::SmartScopedReader<true> lock(shard->mutex); 188 if (shard->allocator.allocated(storage)) 189 return *shard; 190 } 191 } 192 llvm_unreachable("expected storage object to have a valid shard"); 193 } 194 195 /// A thread local cache for storage objects. This helps to reduce the lock 196 /// contention when an object already existing in the cache. 197 ThreadLocalCache<StorageTypeSet> localCache; 198 199 /// A set of uniquer shards to allow for further bucketing accesses for 200 /// instances of this storage type. Each shard is lazily initialized to reduce 201 /// the overhead when only a small amount of shards are in use. 202 std::unique_ptr<std::atomic<Shard *>[]> shards; 203 204 /// The number of available shards. 205 size_t numShards; 206 207 #else 208 /// If multi-threading is disabled, ignore the shard parameter as we will 209 /// always use one shard. 210 ParametricStorageUniquer(size_t numShards = 0) {} 211 212 /// Get or create an instance of a parametric type. 213 BaseStorage * 214 getOrCreate(bool threadingIsEnabled, unsigned hashValue, 215 function_ref<bool(const BaseStorage *)> isEqual, 216 function_ref<BaseStorage *(StorageAllocator &)> ctorFn) { 217 ParametricStorageUniquer::LookupKey lookupKey{hashValue, isEqual}; 218 return getOrCreateUnsafe(shard, lookupKey, ctorFn); 219 } 220 /// Run a mutation function on the provided storage object in a thread-safe 221 /// way. 222 LogicalResult 223 mutate(bool threadingIsEnabled, BaseStorage *storage, 224 function_ref<LogicalResult(StorageAllocator &)> mutationFn) { 225 return mutationFn(shard.allocator); 226 } 227 228 private: 229 /// The main uniquer shard that is used for allocating storage instances. 230 Shard shard; 231 #endif 232 }; 233 } // end anonymous namespace 234 235 namespace mlir { 236 namespace detail { 237 /// This is the implementation of the StorageUniquer class. 238 struct StorageUniquerImpl { 239 using BaseStorage = StorageUniquer::BaseStorage; 240 using StorageAllocator = StorageUniquer::StorageAllocator; 241 242 //===--------------------------------------------------------------------===// 243 // Parametric Storage 244 //===--------------------------------------------------------------------===// 245 246 /// Check if an instance of a parametric storage class exists. 247 bool hasParametricStorage(TypeID id) { return parametricUniquers.count(id); } 248 249 /// Get or create an instance of a parametric type. 250 BaseStorage * 251 getOrCreate(TypeID id, unsigned hashValue, 252 function_ref<bool(const BaseStorage *)> isEqual, 253 function_ref<BaseStorage *(StorageAllocator &)> ctorFn) { 254 assert(parametricUniquers.count(id) && 255 "creating unregistered storage instance"); 256 ParametricStorageUniquer &storageUniquer = *parametricUniquers[id]; 257 return storageUniquer.getOrCreate(threadingIsEnabled, hashValue, isEqual, 258 ctorFn); 259 } 260 261 /// Run a mutation function on the provided storage object in a thread-safe 262 /// way. 263 LogicalResult 264 mutate(TypeID id, BaseStorage *storage, 265 function_ref<LogicalResult(StorageAllocator &)> mutationFn) { 266 assert(parametricUniquers.count(id) && 267 "mutating unregistered storage instance"); 268 ParametricStorageUniquer &storageUniquer = *parametricUniquers[id]; 269 return storageUniquer.mutate(threadingIsEnabled, storage, mutationFn); 270 } 271 272 //===--------------------------------------------------------------------===// 273 // Singleton Storage 274 //===--------------------------------------------------------------------===// 275 276 /// Get or create an instance of a singleton storage class. 277 BaseStorage *getSingleton(TypeID id) { 278 BaseStorage *singletonInstance = singletonInstances[id]; 279 assert(singletonInstance && "expected singleton instance to exist"); 280 return singletonInstance; 281 } 282 283 /// Check if an instance of a singleton storage class exists. 284 bool hasSingleton(TypeID id) const { return singletonInstances.count(id); } 285 286 //===--------------------------------------------------------------------===// 287 // Instance Storage 288 //===--------------------------------------------------------------------===// 289 290 /// Map of type ids to the storage uniquer to use for registered objects. 291 DenseMap<TypeID, std::unique_ptr<ParametricStorageUniquer>> 292 parametricUniquers; 293 294 /// Map of type ids to a singleton instance when the storage class is a 295 /// singleton. 296 DenseMap<TypeID, BaseStorage *> singletonInstances; 297 298 /// Allocator used for uniquing singleton instances. 299 StorageAllocator singletonAllocator; 300 301 /// Flag specifying if multi-threading is enabled within the uniquer. 302 bool threadingIsEnabled = true; 303 }; 304 } // end namespace detail 305 } // namespace mlir 306 307 StorageUniquer::StorageUniquer() : impl(new StorageUniquerImpl()) {} 308 StorageUniquer::~StorageUniquer() {} 309 310 /// Set the flag specifying if multi-threading is disabled within the uniquer. 311 void StorageUniquer::disableMultithreading(bool disable) { 312 impl->threadingIsEnabled = !disable; 313 } 314 315 /// Implementation for getting/creating an instance of a derived type with 316 /// parametric storage. 317 auto StorageUniquer::getParametricStorageTypeImpl( 318 TypeID id, unsigned hashValue, 319 function_ref<bool(const BaseStorage *)> isEqual, 320 function_ref<BaseStorage *(StorageAllocator &)> ctorFn) -> BaseStorage * { 321 return impl->getOrCreate(id, hashValue, isEqual, ctorFn); 322 } 323 324 /// Implementation for registering an instance of a derived type with 325 /// parametric storage. 326 void StorageUniquer::registerParametricStorageTypeImpl(TypeID id) { 327 impl->parametricUniquers.try_emplace( 328 id, std::make_unique<ParametricStorageUniquer>()); 329 } 330 331 /// Implementation for getting an instance of a derived type with default 332 /// storage. 333 auto StorageUniquer::getSingletonImpl(TypeID id) -> BaseStorage * { 334 return impl->getSingleton(id); 335 } 336 337 /// Test is the storage singleton is initialized. 338 bool StorageUniquer::isSingletonStorageInitialized(TypeID id) { 339 return impl->hasSingleton(id); 340 } 341 342 /// Test is the parametric storage is initialized. 343 bool StorageUniquer::isParametricStorageInitialized(TypeID id) { 344 return impl->hasParametricStorage(id); 345 } 346 347 /// Implementation for registering an instance of a derived type with default 348 /// storage. 349 void StorageUniquer::registerSingletonImpl( 350 TypeID id, function_ref<BaseStorage *(StorageAllocator &)> ctorFn) { 351 assert(!impl->singletonInstances.count(id) && 352 "storage class already registered"); 353 impl->singletonInstances.try_emplace(id, ctorFn(impl->singletonAllocator)); 354 } 355 356 /// Implementation for mutating an instance of a derived storage. 357 LogicalResult StorageUniquer::mutateImpl( 358 TypeID id, BaseStorage *storage, 359 function_ref<LogicalResult(StorageAllocator &)> mutationFn) { 360 return impl->mutate(id, storage, mutationFn); 361 } 362