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 {
HashedStorage__anon95dc17a30111::ParametricStorageUniquer::HashedStorage43 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 {
getEmptyKey__anon95dc17a30111::ParametricStorageUniquer::StorageKeyInfo51 static inline HashedStorage getEmptyKey() {
52 return HashedStorage(0, DenseMapInfo<BaseStorage *>::getEmptyKey());
53 }
getTombstoneKey__anon95dc17a30111::ParametricStorageUniquer::StorageKeyInfo54 static inline HashedStorage getTombstoneKey() {
55 return HashedStorage(0, DenseMapInfo<BaseStorage *>::getTombstoneKey());
56 }
57
getHashValue__anon95dc17a30111::ParametricStorageUniquer::StorageKeyInfo58 static inline unsigned getHashValue(const HashedStorage &key) {
59 return key.hashValue;
60 }
getHashValue__anon95dc17a30111::ParametricStorageUniquer::StorageKeyInfo61 static inline unsigned getHashValue(const LookupKey &key) {
62 return key.hashValue;
63 }
64
isEqual__anon95dc17a30111::ParametricStorageUniquer::StorageKeyInfo65 static inline bool isEqual(const HashedStorage &lhs,
66 const HashedStorage &rhs) {
67 return lhs.storage == rhs.storage;
68 }
isEqual__anon95dc17a30111::ParametricStorageUniquer::StorageKeyInfo69 static inline bool isEqual(const LookupKey &lhs, const HashedStorage &rhs) {
70 if (isEqual(rhs, getEmptyKey()) || isEqual(rhs, getTombstoneKey()))
71 return false;
72 // Invoke the equality function on the lookup key.
73 return lhs.isEqual(rhs.storage);
74 }
75 };
76 using StorageTypeSet = DenseSet<HashedStorage, StorageKeyInfo>;
77
78 /// This class represents a single shard of the uniquer. The uniquer uses a
79 /// set of shards to allow for multiple threads to create instances with less
80 /// lock contention.
81 struct Shard {
82 /// The set containing the allocated storage instances.
83 StorageTypeSet instances;
84
85 /// Allocator to use when constructing derived instances.
86 StorageAllocator allocator;
87
88 #if LLVM_ENABLE_THREADS != 0
89 /// A mutex to keep uniquing thread-safe.
90 llvm::sys::SmartRWMutex<true> mutex;
91 #endif
92 };
93
94 /// Get or create an instance of a param derived type in an thread-unsafe
95 /// fashion.
96 BaseStorage *
getOrCreateUnsafe(Shard & shard,LookupKey & key,function_ref<BaseStorage * (StorageAllocator &)> ctorFn)97 getOrCreateUnsafe(Shard &shard, LookupKey &key,
98 function_ref<BaseStorage *(StorageAllocator &)> ctorFn) {
99 auto existing = shard.instances.insert_as({key.hashValue}, key);
100 BaseStorage *&storage = existing.first->storage;
101 if (existing.second)
102 storage = ctorFn(shard.allocator);
103 return storage;
104 }
105
106 /// Destroy all of the storage instances within the given shard.
destroyShardInstances(Shard & shard)107 void destroyShardInstances(Shard &shard) {
108 if (!destructorFn)
109 return;
110 for (HashedStorage &instance : shard.instances)
111 destructorFn(instance.storage);
112 }
113
114 public:
115 #if LLVM_ENABLE_THREADS != 0
116 /// Initialize the storage uniquer with a given number of storage shards to
117 /// use. The provided shard number is required to be a valid power of 2. The
118 /// destructor function is used to destroy any allocated storage instances.
ParametricStorageUniquer(function_ref<void (BaseStorage *)> destructorFn,size_t numShards=8)119 ParametricStorageUniquer(function_ref<void(BaseStorage *)> destructorFn,
120 size_t numShards = 8)
121 : shards(new std::atomic<Shard *>[numShards]), numShards(numShards),
122 destructorFn(destructorFn) {
123 assert(llvm::isPowerOf2_64(numShards) &&
124 "the number of shards is required to be a power of 2");
125 for (size_t i = 0; i < numShards; i++)
126 shards[i].store(nullptr, std::memory_order_relaxed);
127 }
~ParametricStorageUniquer()128 ~ParametricStorageUniquer() {
129 // Free all of the allocated shards.
130 for (size_t i = 0; i != numShards; ++i) {
131 if (Shard *shard = shards[i].load()) {
132 destroyShardInstances(*shard);
133 delete shard;
134 }
135 }
136 }
137 /// Get or create an instance of a parametric type.
138 BaseStorage *
getOrCreate(bool threadingIsEnabled,unsigned hashValue,function_ref<bool (const BaseStorage *)> isEqual,function_ref<BaseStorage * (StorageAllocator &)> ctorFn)139 getOrCreate(bool threadingIsEnabled, unsigned hashValue,
140 function_ref<bool(const BaseStorage *)> isEqual,
141 function_ref<BaseStorage *(StorageAllocator &)> ctorFn) {
142 Shard &shard = getShard(hashValue);
143 ParametricStorageUniquer::LookupKey lookupKey{hashValue, isEqual};
144 if (!threadingIsEnabled)
145 return getOrCreateUnsafe(shard, lookupKey, ctorFn);
146
147 // Check for a instance of this object in the local cache.
148 auto localIt = localCache->insert_as({hashValue}, lookupKey);
149 BaseStorage *&localInst = localIt.first->storage;
150 if (localInst)
151 return localInst;
152
153 // Check for an existing instance in read-only mode.
154 {
155 llvm::sys::SmartScopedReader<true> typeLock(shard.mutex);
156 auto it = shard.instances.find_as(lookupKey);
157 if (it != shard.instances.end())
158 return localInst = it->storage;
159 }
160
161 // Acquire a writer-lock so that we can safely create the new storage
162 // instance.
163 llvm::sys::SmartScopedWriter<true> typeLock(shard.mutex);
164 return localInst = getOrCreateUnsafe(shard, lookupKey, ctorFn);
165 }
166 /// Run a mutation function on the provided storage object in a thread-safe
167 /// way.
168 LogicalResult
mutate(bool threadingIsEnabled,BaseStorage * storage,function_ref<LogicalResult (StorageAllocator &)> mutationFn)169 mutate(bool threadingIsEnabled, BaseStorage *storage,
170 function_ref<LogicalResult(StorageAllocator &)> mutationFn) {
171 Shard &shard = getShardFor(storage);
172 if (!threadingIsEnabled)
173 return mutationFn(shard.allocator);
174
175 llvm::sys::SmartScopedWriter<true> lock(shard.mutex);
176 return mutationFn(shard.allocator);
177 }
178
179 private:
180 /// Return the shard used for the given hash value.
getShard(unsigned hashValue)181 Shard &getShard(unsigned hashValue) {
182 // Get a shard number from the provided hashvalue.
183 unsigned shardNum = hashValue & (numShards - 1);
184
185 // Try to acquire an already initialized shard.
186 Shard *shard = shards[shardNum].load(std::memory_order_acquire);
187 if (shard)
188 return *shard;
189
190 // Otherwise, try to allocate a new shard.
191 Shard *newShard = new Shard();
192 if (shards[shardNum].compare_exchange_strong(shard, newShard))
193 return *newShard;
194
195 // If one was allocated before we can initialize ours, delete ours.
196 delete newShard;
197 return *shard;
198 }
199
200 /// Return the shard that allocated the provided storage object.
getShardFor(BaseStorage * storage)201 Shard &getShardFor(BaseStorage *storage) {
202 for (size_t i = 0; i != numShards; ++i) {
203 if (Shard *shard = shards[i].load(std::memory_order_acquire)) {
204 llvm::sys::SmartScopedReader<true> lock(shard->mutex);
205 if (shard->allocator.allocated(storage))
206 return *shard;
207 }
208 }
209 llvm_unreachable("expected storage object to have a valid shard");
210 }
211
212 /// A thread local cache for storage objects. This helps to reduce the lock
213 /// contention when an object already existing in the cache.
214 ThreadLocalCache<StorageTypeSet> localCache;
215
216 /// A set of uniquer shards to allow for further bucketing accesses for
217 /// instances of this storage type. Each shard is lazily initialized to reduce
218 /// the overhead when only a small amount of shards are in use.
219 std::unique_ptr<std::atomic<Shard *>[]> shards;
220
221 /// The number of available shards.
222 size_t numShards;
223
224 /// Function to used to destruct any allocated storage instances.
225 function_ref<void(BaseStorage *)> destructorFn;
226
227 #else
228 /// If multi-threading is disabled, ignore the shard parameter as we will
229 /// always use one shard. The destructor function is used to destroy any
230 /// allocated storage instances.
231 ParametricStorageUniquer(function_ref<void(BaseStorage *)> destructorFn,
232 size_t numShards = 0)
233 : destructorFn(destructorFn) {}
234 ~ParametricStorageUniquer() { destroyShardInstances(shard); }
235
236 /// Get or create an instance of a parametric type.
237 BaseStorage *
238 getOrCreate(bool threadingIsEnabled, unsigned hashValue,
239 function_ref<bool(const BaseStorage *)> isEqual,
240 function_ref<BaseStorage *(StorageAllocator &)> ctorFn) {
241 ParametricStorageUniquer::LookupKey lookupKey{hashValue, isEqual};
242 return getOrCreateUnsafe(shard, lookupKey, ctorFn);
243 }
244 /// Run a mutation function on the provided storage object in a thread-safe
245 /// way.
246 LogicalResult
247 mutate(bool threadingIsEnabled, BaseStorage *storage,
248 function_ref<LogicalResult(StorageAllocator &)> mutationFn) {
249 return mutationFn(shard.allocator);
250 }
251
252 private:
253 /// The main uniquer shard that is used for allocating storage instances.
254 Shard shard;
255
256 /// Function to used to destruct any allocated storage instances.
257 function_ref<void(BaseStorage *)> destructorFn;
258 #endif
259 };
260 } // namespace
261
262 namespace mlir {
263 namespace detail {
264 /// This is the implementation of the StorageUniquer class.
265 struct StorageUniquerImpl {
266 using BaseStorage = StorageUniquer::BaseStorage;
267 using StorageAllocator = StorageUniquer::StorageAllocator;
268
269 //===--------------------------------------------------------------------===//
270 // Parametric Storage
271 //===--------------------------------------------------------------------===//
272
273 /// Check if an instance of a parametric storage class exists.
hasParametricStoragemlir::detail::StorageUniquerImpl274 bool hasParametricStorage(TypeID id) { return parametricUniquers.count(id); }
275
276 /// Get or create an instance of a parametric type.
277 BaseStorage *
getOrCreatemlir::detail::StorageUniquerImpl278 getOrCreate(TypeID id, unsigned hashValue,
279 function_ref<bool(const BaseStorage *)> isEqual,
280 function_ref<BaseStorage *(StorageAllocator &)> ctorFn) {
281 assert(parametricUniquers.count(id) &&
282 "creating unregistered storage instance");
283 ParametricStorageUniquer &storageUniquer = *parametricUniquers[id];
284 return storageUniquer.getOrCreate(threadingIsEnabled, hashValue, isEqual,
285 ctorFn);
286 }
287
288 /// Run a mutation function on the provided storage object in a thread-safe
289 /// way.
290 LogicalResult
mutatemlir::detail::StorageUniquerImpl291 mutate(TypeID id, BaseStorage *storage,
292 function_ref<LogicalResult(StorageAllocator &)> mutationFn) {
293 assert(parametricUniquers.count(id) &&
294 "mutating unregistered storage instance");
295 ParametricStorageUniquer &storageUniquer = *parametricUniquers[id];
296 return storageUniquer.mutate(threadingIsEnabled, storage, mutationFn);
297 }
298
299 //===--------------------------------------------------------------------===//
300 // Singleton Storage
301 //===--------------------------------------------------------------------===//
302
303 /// Get or create an instance of a singleton storage class.
getSingletonmlir::detail::StorageUniquerImpl304 BaseStorage *getSingleton(TypeID id) {
305 BaseStorage *singletonInstance = singletonInstances[id];
306 assert(singletonInstance && "expected singleton instance to exist");
307 return singletonInstance;
308 }
309
310 /// Check if an instance of a singleton storage class exists.
hasSingletonmlir::detail::StorageUniquerImpl311 bool hasSingleton(TypeID id) const { return singletonInstances.count(id); }
312
313 //===--------------------------------------------------------------------===//
314 // Instance Storage
315 //===--------------------------------------------------------------------===//
316
317 /// Map of type ids to the storage uniquer to use for registered objects.
318 DenseMap<TypeID, std::unique_ptr<ParametricStorageUniquer>>
319 parametricUniquers;
320
321 /// Map of type ids to a singleton instance when the storage class is a
322 /// singleton.
323 DenseMap<TypeID, BaseStorage *> singletonInstances;
324
325 /// Allocator used for uniquing singleton instances.
326 StorageAllocator singletonAllocator;
327
328 /// Flag specifying if multi-threading is enabled within the uniquer.
329 bool threadingIsEnabled = true;
330 };
331 } // namespace detail
332 } // namespace mlir
333
StorageUniquer()334 StorageUniquer::StorageUniquer() : impl(new StorageUniquerImpl()) {}
335 StorageUniquer::~StorageUniquer() = default;
336
337 /// Set the flag specifying if multi-threading is disabled within the uniquer.
disableMultithreading(bool disable)338 void StorageUniquer::disableMultithreading(bool disable) {
339 impl->threadingIsEnabled = !disable;
340 }
341
342 /// Implementation for getting/creating an instance of a derived type with
343 /// parametric storage.
getParametricStorageTypeImpl(TypeID id,unsigned hashValue,function_ref<bool (const BaseStorage *)> isEqual,function_ref<BaseStorage * (StorageAllocator &)> ctorFn)344 auto StorageUniquer::getParametricStorageTypeImpl(
345 TypeID id, unsigned hashValue,
346 function_ref<bool(const BaseStorage *)> isEqual,
347 function_ref<BaseStorage *(StorageAllocator &)> ctorFn) -> BaseStorage * {
348 return impl->getOrCreate(id, hashValue, isEqual, ctorFn);
349 }
350
351 /// Implementation for registering an instance of a derived type with
352 /// parametric storage.
registerParametricStorageTypeImpl(TypeID id,function_ref<void (BaseStorage *)> destructorFn)353 void StorageUniquer::registerParametricStorageTypeImpl(
354 TypeID id, function_ref<void(BaseStorage *)> destructorFn) {
355 impl->parametricUniquers.try_emplace(
356 id, std::make_unique<ParametricStorageUniquer>(destructorFn));
357 }
358
359 /// Implementation for getting an instance of a derived type with default
360 /// storage.
getSingletonImpl(TypeID id)361 auto StorageUniquer::getSingletonImpl(TypeID id) -> BaseStorage * {
362 return impl->getSingleton(id);
363 }
364
365 /// Test is the storage singleton is initialized.
isSingletonStorageInitialized(TypeID id)366 bool StorageUniquer::isSingletonStorageInitialized(TypeID id) {
367 return impl->hasSingleton(id);
368 }
369
370 /// Test is the parametric storage is initialized.
isParametricStorageInitialized(TypeID id)371 bool StorageUniquer::isParametricStorageInitialized(TypeID id) {
372 return impl->hasParametricStorage(id);
373 }
374
375 /// Implementation for registering an instance of a derived type with default
376 /// storage.
registerSingletonImpl(TypeID id,function_ref<BaseStorage * (StorageAllocator &)> ctorFn)377 void StorageUniquer::registerSingletonImpl(
378 TypeID id, function_ref<BaseStorage *(StorageAllocator &)> ctorFn) {
379 assert(!impl->singletonInstances.count(id) &&
380 "storage class already registered");
381 impl->singletonInstances.try_emplace(id, ctorFn(impl->singletonAllocator));
382 }
383
384 /// Implementation for mutating an instance of a derived storage.
mutateImpl(TypeID id,BaseStorage * storage,function_ref<LogicalResult (StorageAllocator &)> mutationFn)385 LogicalResult StorageUniquer::mutateImpl(
386 TypeID id, BaseStorage *storage,
387 function_ref<LogicalResult(StorageAllocator &)> mutationFn) {
388 return impl->mutate(id, storage, mutationFn);
389 }
390