Cholla 3.0.1-dev
Cholla - Massively parallel hydro on GPUs
Loading...
Searching...
No Matches
shared.h
Go to the documentation of this file.
1
5#pragma once
6
7#include <atomic>
8#include <cstddef> // std::ptrdiff_t
9#include <type_traits>
10
159namespace detail
160{
161
162// define logic for reference counting (on the host)
163// - this default implementation uses built-in atomic operations provided with
164// the C++ standard library
165// - this logic has been pull out of ControlBlock in case we want to take a
166// crack at implementing an alternative version in terms of OpenMP constructs
167// (this requires the use of OpenMP 3.1 or newer)
168// - for context, the use of std::atomic with OpenMP **PROBABLY** works as you
169// would expect, but that is not strictly required by the OpenMP standard. In
170// practice compiler writers generally try to "do the right thing" and make
171// things work correctly. Furthermore, the kind of logic where this plays a
172// role is typically executed at the very start and end of Cholla's execution
173// (i.e. outside of all OpenMP blocks)
174//
175// optimization opportunity: the memory ordering is a little stricter than
176// necessary (out of an abundance of cautious), which introduces additional
177// overhead. In practice, this shouldn't matter very much in cholla since we
178// increment/decrement reference counts relatively infrequently; this happens
179// when initializing relevant datastructures and when they go out of scope
180// (which is almost entirely constrained to startup and shutdown)
181
182using RefCountType = std::atomic<long>;
184inline long ref_count_increment(RefCountType& count) noexcept { return count.fetch_add(1L, std::memory_order_seq_cst); }
186inline long ref_count_decrement(RefCountType& count) noexcept
187{
188 return count.fetch_add(-1L, std::memory_order_seq_cst);
189}
190
203{
204 RefCountType ref_count_;
205
206 protected:
207 virtual ~ControlBlock() noexcept {}
208
209 public:
210 ControlBlock() noexcept : ref_count_{1L} {}
211
213 void increment_count() noexcept
214 {
215 long preincrement_val = ref_count_increment(ref_count_);
216 CHOLLA_ASSERT(preincrement_val >= 1L, "invariant is violated"); // <- sanity check!
217 }
218
220 void decrement_count() noexcept
221 {
222 long predecrement_val = ref_count_decrement(ref_count_);
223 CHOLLA_ASSERT(predecrement_val > 0L, "invariant is violated"); // <- sanity check!
224 if (predecrement_val == 1L) delete this;
225 }
226};
227
228template <typename HandleOrPtrType, typename Deleter>
230{
231 HandleOrPtrType managed_;
232 Deleter deleter_;
233
234 public:
235 ControlBlockImpl(HandleOrPtrType& m, Deleter& d) : ControlBlock(), managed_{m}, deleter_{d} {}
236
237 ~ControlBlockImpl() { deleter_(managed_); }
238};
239
240} // namespace detail
241
242// down below, we move on to actually defining SharedHandle and SharedDevPtr.
243
244// define the CALL_INCREMENT_COUNT and CALL_DECREMENT_COUNT macros
245// -> these macros forward onto detail::ControlBlock::increment_count and
246// detail::ControlBlock::decrement_count when invoked on the host and do nothing
247// when invoked on the host
248// -> this is the desired behavior we want (it's explained in more detail at the top of
249// this page)
250#if defined(__CUDA_ARCH__) || defined(__HIP_DEVICE_COMPILE__)
251 #define CALL_INCREMENT_COUNT(cb_ptr) /* DOES NOTHING ON DEVICE */
252 #define CALL_DECREMENT_COUNT(cb_ptr) /* DOES NOTHING ON DEVICE */
253#else
254 #define CALL_INCREMENT_COUNT(cb_ptr) (cb_ptr)->increment_count()
255 #define CALL_DECREMENT_COUNT(cb_ptr) (cb_ptr)->decrement_count()
256#endif
257
267#define DEFINE_COMMON_METHODS(KLASS) \
268 /* The lint that we disable just below tells us that we should enclose every occurrence of KLASS within */ \
269 /* parentheses. Basically, we'd write it as (KLASS). I'm not actually sure that the result of the macro */ \
270 /* expansion would be valid code and it would certainly make things harder to read. This check is most */ \
271 /* useful when you would interject a macro call inside a C++ expression like a function. Since this macro */ \
272 /* is used in a fundamentally different way (i.e. to define functions) its okay to disable the check */ \
273 /* NOLINTBEGIN(bugprone-macro-parentheses) */ \
274 \
275 /* implement the copy constructor */ \
276 template <typename T> \
277 __host__ __device__ KLASS<T>::KLASS(const KLASS<T>& other) noexcept : wrapped_{other.wrapped_}, cb_{other.cb_} \
278 { \
279 if (cb_ != nullptr) { \
280 /* The lint that we disable just below tells us that we should enclose every is warning about using */ \
281 /* memory after it is freed. However, the fact that we always initialize the cb_ to nullptr and */ \
282 /* that we only decrement the reference count if we overwrite cb_ immediately afterwards means that */ \
283 /* it's impossible for this scenario to arise */ \
284 /* NOLINTBEGIN(clang-analyzer-cplusplus.NewDelete,-warnings-as-errors) */ \
285 CALL_INCREMENT_COUNT(cb_); \
286 /* NOLINTEND(clang-analyzer-cplusplus.NewDelete,-warnings-as-errors) */ \
287 } \
288 } \
289 \
290 /* implement the move constructor */ \
291 template <typename T> \
292 __host__ __device__ KLASS<T>::KLASS(KLASS<T>&& other) noexcept : wrapped_{other.wrapped_}, cb_{other.cb_} \
293 { \
294 other.set_empty_wrapped_(); \
295 other.cb_ = nullptr; \
296 } \
297 \
298 /* implement the copy assignment operation */ \
299 template <typename T> \
300 __host__ __device__ KLASS<T>& KLASS<T>::operator=(const KLASS<T>& other) noexcept \
301 { \
302 /* care needs to be taken for self-assignment when `this` is the ONLY owner of a resource (it would be */ \
303 /* bad if a naive implementation decremented the reference count to zero before trying to increment it) */ \
304 /* -> at the time of writing, the rest of the implementation actually makes this branch unnecessary. */ \
305 /* For now, we explicitly handle this case, since its more idiomatic (plus it addresses the */ \
306 /* associated clang-tidy warning) */ \
307 if (this == &other) return *this; \
308 \
309 /* the different_control_blocks check is a minor optimization. It lets us avoid an unnecessary pair of */ \
310 /* atomic increments and decrements (the fact they are atomic makes the operation more expensive). This */ \
311 /* is a no-brainer since we already branch based on whether cb_ or other.cb_ are null pointers */ \
312 bool different_control_blocks = cb_ != other.cb_; \
313 if (different_control_blocks and (other.cb_ != nullptr)) CALL_INCREMENT_COUNT(other.cb_); \
314 if (different_control_blocks and (cb_ != nullptr)) CALL_DECREMENT_COUNT(cb_); \
315 cb_ = other.cb_; \
316 wrapped_ = other.wrapped_; \
317 return *this; \
318 } \
319 \
320 /* implement the move assignment operation */ \
321 template <typename T> \
322 __host__ __device__ KLASS<T>& KLASS<T>::operator=(KLASS<T>&& other) noexcept \
323 { \
324 reset(); \
325 swap(other); \
326 return *this; \
327 } \
328 \
329 template <typename T> \
330 __host__ __device__ void KLASS<T>::swap(KLASS<T>& other) noexcept \
331 { \
332 decltype(wrapped_) tmp_wrapped = wrapped_; \
333 wrapped_ = other.wrapped_; \
334 other.wrapped_ = tmp_wrapped; \
335 \
336 detail::ControlBlock* tmp_cb = cb_; \
337 cb_ = other.cb_; \
338 other.cb_ = tmp_cb; \
339 } \
340 \
341 template <typename T> \
342 __host__ __device__ void KLASS<T>::reset() noexcept \
343 { \
344 /* reminder: if the reference count hits 0, the control block automatically: */ \
345 /* - destroys wrapped_ (a copy of wrapped is tracked within the control */ \
346 /* block for this purpose) */ \
347 /* - calls delete on itself */ \
348 if (cb_ != nullptr) { \
349 /* NOLINTBEGIN(clang-analyzer-unix.DynamicMemoryModeling, clang-analyzer-cplusplus.NewDelete) */ \
350 CALL_DECREMENT_COUNT(cb_); \
351 /* NOLINTEND(clang-analyzer-unix.DynamicMemoryModeling, clang-analyzer-cplusplus.NewDelete) */ \
352 cb_ = nullptr; \
353 set_empty_wrapped_(); \
354 } \
355 } \
356 /* NOLINTEND(bugprone-macro-parentheses) */
357
365template <typename HandleT>
367{
368 // perform some sanity checks:
369 static_assert(std::is_arithmetic_v<HandleT> or std::is_aggregate_v<HandleT> or std::is_pointer_v<HandleT>);
370 static_assert(not std::is_const_v<HandleT>);
371
372 // data members:
373 HandleT wrapped_;
375
376 // helper method used by DEFINE_COMMON_METHODS to set the value of wrapped_ to the
377 // appropriate value when a SharedHandle is "empty"
378 // -> this method does nothing since the value of wrapped_ in an empty SharedHandle
379 // instance is explicitly undefined
380 // -> this is defined for parity with SharedDevPtr
381 __host__ __device__ __forceinline__ void set_empty_wrapped_() const noexcept {}
382
383 public:
384 // used for testing purposes
385 typedef HandleT wrapped_ref_type;
386
395 __host__ __device__ SharedHandle() : wrapped_{}, cb_{nullptr} {};
396
410 template <typename Deleter>
411 __host__ SharedHandle(HandleT handle, Deleter d)
412 // NOLINTNEXTLINE(clang-analyzer-unix.DynamicMemoryModeling, clang-analyzer-cplusplus.NewDelete)
413 : wrapped_{handle}, cb_{new detail::ControlBlockImpl<HandleT, Deleter>(handle, d)}
414 {
415 }
416
418 __host__ __device__ ~SharedHandle() noexcept { reset(); }
419
420 // copy/move construction and assignment
421 __host__ __device__ SharedHandle(const SharedHandle& other) noexcept;
422 __host__ __device__ SharedHandle(SharedHandle&& other) noexcept;
423 __host__ __device__ SharedHandle& operator=(const SharedHandle& other) noexcept;
424 __host__ __device__ SharedHandle& operator=(SharedHandle&& other) noexcept;
425
427 __host__ __device__ void swap(SharedHandle& o) noexcept;
428
433 __host__ __device__ void reset() noexcept;
434
436 __host__ __device__ __forceinline__ HandleT get() const noexcept { return wrapped_; }
437
439 __host__ __device__ explicit operator bool() const noexcept { return cb_ != nullptr; }
440};
441
442// provide definitions for the remainder of methods
444
445
450template <typename T>
452{
453 // data members:
454 T* wrapped_;
456
457 // helper method used by DEFINE_COMMON_METHODS to set the value of wrapped_ to the
458 // appropriate value when a SharedDevPtr is "empty"
459 __host__ __device__ __forceinline__ void set_empty_wrapped_() noexcept { wrapped_ = nullptr; }
460
461 public:
462 // used for testing purposes
463 typedef T* wrapped_ref_type;
464
466 __host__ __device__ SharedDevPtr() : wrapped_{nullptr}, cb_{nullptr} {}
467
481 template <typename Deleter>
482 __host__ SharedDevPtr(T* ptr, Deleter d)
483 // NOLINTNEXTLINE(clang-analyzer-unix.DynamicMemoryModeling, clang-analyzer-cplusplus.NewDelete)
484 : wrapped_{ptr}, cb_{new detail::ControlBlockImpl<T*, Deleter>(ptr, d)}
485 {
486 }
487
489 __host__ __device__ ~SharedDevPtr() noexcept { reset(); }
490
491 // copy/move construction and assignment
492 __host__ __device__ SharedDevPtr(const SharedDevPtr& other) noexcept;
493 __host__ __device__ SharedDevPtr(SharedDevPtr&& other) noexcept;
494 __host__ __device__ SharedDevPtr& operator=(const SharedDevPtr& other) noexcept;
495 __host__ __device__ SharedDevPtr& operator=(SharedDevPtr&& other) noexcept;
496
498 __host__ __device__ void swap(SharedDevPtr& other) noexcept;
499
504 __host__ __device__ void reset() noexcept;
505
507 __host__ __device__ __forceinline__ T* get() const noexcept { return wrapped_; }
508
510 __host__ __device__ explicit operator bool() const noexcept { return wrapped_ != nullptr; }
511
519 __device__ __forceinline__ T& operator*() const noexcept { return *wrapped_; }
520
529 __device__ __forceinline__ T& operator[](std::ptrdiff_t idx) const { return wrapped_[idx]; }
530};
531
532// provide definitions for the remainder of methods
534
535// let's do some cleanup to avoid leaking of macros into other parts of the codebase
536#undef CALL_INCREMENT_COUNT
537#undef CALL_DECREMENT_COUNT
538#undef DEFINE_COMMON_METHODS
Wraps a device pointer while providing shared object semantics.
Definition shared.h:452
__host__ __device__ SharedDevPtr()
Default constructor (creates an "empty" instance)
Definition shared.h:466
__host__ __device__ ~SharedDevPtr() noexcept
Destructor.
Definition shared.h:489
__device__ __forceinline__ T & operator*() const noexcept
Dereference the stored pointer.
Definition shared.h:519
__host__ SharedDevPtr(T *ptr, Deleter d)
Primary constructor.
Definition shared.h:482
__host__ __device__ void swap(SharedDevPtr &other) noexcept
swap the contents of this and other
__device__ __forceinline__ T & operator[](std::ptrdiff_t idx) const
Dereference the stored pointer.
Definition shared.h:529
__host__ __device__ __forceinline__ T * get() const noexcept
Return the stored pointer.
Definition shared.h:507
__host__ __device__ void reset() noexcept
Release ownership of the owned resource (if any)
Wraps a handles while providing shared object semantics.
Definition shared.h:367
__host__ SharedHandle(HandleT handle, Deleter d)
Primary constructor.
Definition shared.h:411
__host__ __device__ void swap(SharedHandle &o) noexcept
swap the contents of this and other
__host__ __device__ __forceinline__ HandleT get() const noexcept
Return the stored handle.
Definition shared.h:436
__host__ __device__ ~SharedHandle() noexcept
Destructor.
Definition shared.h:418
__host__ __device__ void reset() noexcept
Release ownership of the owned resource (if any)
__host__ __device__ SharedHandle()
Default Constructor (constructs an empty instance)
Definition shared.h:395
Definition shared.h:230
Helps implement SharedHandle & SharedDevPtr.
Definition shared.h:203
void increment_count() noexcept
increment reference count
Definition shared.h:213
void decrement_count() noexcept
decrement ref count & trigger destructor of this if the count hits 0
Definition shared.h:220
#define DEFINE_COMMON_METHODS(KLASS)
Implements common methods of SharedHandle and SharedDevPtr.
Definition shared.h:267
long ref_count_increment(RefCountType &count) noexcept
Definition shared.h:184
long ref_count_decrement(RefCountType &count) noexcept
Definition shared.h:186