dispenso 1.6.2
A library for task parallelism
Loading...
Searching...
No Matches
concurrent_object_arena.h
Go to the documentation of this file.
1/*
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 */
7
14#pragma once
15
16#include <dispenso/platform.h>
17
18#include <algorithm>
19#include <atomic>
20#include <cassert>
21#include <cstring>
22#include <mutex>
23#include <type_traits>
24#include <utility>
25#include <vector>
26
27namespace detail {
28
29template <class T>
30constexpr T log2i(const T v) {
31 T log2 = 0, val = v;
32 while (val >>= 1)
33 ++log2;
34 return log2;
35}
36
37} // namespace detail
38
39namespace dispenso {
68template <class T, class Index = size_t, size_t alignment = dispenso::kCacheLineSize>
70 ConcurrentObjectArena() = delete;
78 explicit ConcurrentObjectArena(const Index minBuffSize, const Index initialSize = 0)
79 : kLog2BuffSize(
80 ::detail::log2i(minBuffSize) +
81 ((Index{1} << ::detail::log2i(minBuffSize)) == minBuffSize ? 0 : 1)),
82 kBufferSize(Index{1} << kLog2BuffSize),
83 kMask((Index{1} << kLog2BuffSize) - 1),
84 pos_(0),
85 allocatedSize_(0),
86 buffers_(nullptr),
87 buffersSize_(0),
88 buffersPos_(0) {
89 allocateBuffer();
90 allocatedSize_.store(kBufferSize, std::memory_order_relaxed);
91 if (initialSize > 0) {
92 grow_by(initialSize);
93 }
94 }
95
100 : kLog2BuffSize(other.kLog2BuffSize),
101 kBufferSize(other.kBufferSize),
102 kMask(other.kMask),
103 pos_(other.pos_.load(std::memory_order_relaxed)),
104 allocatedSize_(other.allocatedSize_.load(std::memory_order_relaxed)),
105 buffersSize_(other.buffersSize_),
106 buffersPos_(other.buffersPos_),
107 gaps_(other.gaps_) {
108 static_assert(
109 std::is_trivially_copyable<T>::value,
110 "ConcurrentObjectArena copy constructor uses memcpy; T must be trivially copyable.");
111 T** otherBuffers = other.buffers_.load(std::memory_order_acquire);
112 T** newBuffers = new T*[buffersSize_];
113 // Only the first buffersPos_ slots of the pointer table hold buffers; the rest are
114 // uninitialized spare capacity, filled in by allocateBuffer() as the arena grows.
115 for (Index i = 0; i < buffersPos_; ++i) {
116 void* ptr = detail::alignedMallocOrThrow(kBufferSize * sizeof(T), alignment);
117 std::memcpy(ptr, otherBuffers[i], kBufferSize * sizeof(T));
118 newBuffers[i] = static_cast<T*>(ptr);
119 }
120 buffers_.store(newBuffers, std::memory_order_release);
121 }
122
127 : kLog2BuffSize(0),
128 kBufferSize(0),
129 kMask(0),
130 pos_(0),
131 allocatedSize_(0),
132 buffers_(nullptr),
133 buffersSize_(0),
134 buffersPos_(0) {
135 swap(*this, other);
136 }
137
139 T** buffers = buffers_.load(std::memory_order_acquire);
140
141 destroyObjects(buffers);
142
143 for (Index i = 0; i < buffersPos_; i++)
144 detail::alignedFree(buffers[i]);
145
146 delete[] buffers;
147
148 for (T** p : deleteLater_)
149 delete[] p;
150 }
151
155 ConcurrentObjectArena<T, Index, alignment>& operator=(
156 ConcurrentObjectArena<T, Index, alignment> const& other) {
157 ConcurrentObjectArena<T, Index, alignment> copy(other);
158 swap(*this, copy);
159 return *this;
160 }
161
165 ConcurrentObjectArena<T, Index, alignment>& operator=(
166 ConcurrentObjectArena<T, Index, alignment>&& other) noexcept {
167 swap(*this, other);
168 return *this;
169 }
182 Index grow_by(const Index delta) {
183 Index newPos;
184 Index oldPos = pos_.load(std::memory_order_relaxed);
185
186 do {
187 Index curSize = allocatedSize_.load(std::memory_order_acquire);
188
189 if (oldPos + delta >= curSize) {
190 const std::lock_guard<std::mutex> guard(resizeMutex_);
191 curSize = allocatedSize_.load(std::memory_order_relaxed);
192 while (oldPos + delta >= curSize) {
193 allocateBuffer();
194 allocatedSize_.store(curSize + kBufferSize, std::memory_order_release);
195 curSize = curSize + kBufferSize;
196 }
197 }
198
199 newPos = oldPos + delta;
200 } while (!std::atomic_compare_exchange_weak_explicit(
201 &pos_, &oldPos, newPos, std::memory_order_release, std::memory_order_relaxed));
202
203 Index built = oldPos;
204#if defined(__cpp_exceptions)
205 try {
206 constructObjects(oldPos, newPos, built);
207 } catch (...) {
208 destroyRange(buffers_.load(std::memory_order_acquire), oldPos, built);
209 recordGap(oldPos, newPos);
210 throw;
211 }
212#else
213 constructObjects(oldPos, newPos, built);
214#endif // __cpp_exceptions
215
216 return oldPos;
217 }
218
227 inline const T& operator[](const Index index) const {
228 const Index bufIndex = index >> kLog2BuffSize;
229 const Index i = index & kMask;
230
231 return buffers_.load(std::memory_order_acquire)[bufIndex][i];
232 }
233
242 inline T& operator[](const Index index) {
243 return const_cast<T&>(
244 const_cast<const ConcurrentObjectArena<T, Index, alignment>&>(*this)[index]);
245 }
246
252 Index size() const {
253 return pos_.load(std::memory_order_relaxed);
254 }
255
260 Index capacity() const {
261 return allocatedSize_.load(std::memory_order_relaxed);
262 }
263
268 Index numBuffers() const {
269 return buffersPos_;
270 }
271
277 const T* getBuffer(const Index index) const {
278 return buffers_.load(std::memory_order_acquire)[index];
279 }
280
286 T* getBuffer(const Index index) {
287 return buffers_.load(std::memory_order_acquire)[index];
288 }
289
295 Index getBufferSize(const Index index) const {
296 const Index numBuffs = numBuffers();
297 assert(index < numBuffs);
298
299 if (index < numBuffs - 1)
300 return kBufferSize;
301 else
302 return pos_.load(std::memory_order_relaxed) - (kBufferSize * (numBuffs - 1));
303 }
304
310 friend void swap(
313 using std::swap;
314
315 swap(lhs.kLog2BuffSize, rhs.kLog2BuffSize);
316 swap(lhs.kBufferSize, rhs.kBufferSize);
317 swap(lhs.kMask, rhs.kMask);
318
319 const Index rhs_pos = rhs.pos_.load(std::memory_order_relaxed);
320 rhs.pos_.store(lhs.pos_.load(std::memory_order_relaxed), std::memory_order_relaxed);
321 lhs.pos_.store(rhs_pos, std::memory_order_relaxed);
322
323 const Index rhs_allocatedSize = rhs.allocatedSize_.load(std::memory_order_relaxed);
324 rhs.allocatedSize_.store(
325 lhs.allocatedSize_.load(std::memory_order_relaxed), std::memory_order_relaxed);
326 lhs.allocatedSize_.store(rhs_allocatedSize, std::memory_order_relaxed);
327
328 T** const rhs_buffers = rhs.buffers_.load(std::memory_order_acquire);
329 rhs.buffers_.store(lhs.buffers_.load(std::memory_order_acquire), std::memory_order_release);
330 lhs.buffers_.store(rhs_buffers, std::memory_order_release);
331
332 swap(lhs.buffersSize_, rhs.buffersSize_);
333 swap(lhs.buffersPos_, rhs.buffersPos_);
334 swap(lhs.deleteLater_, rhs.deleteLater_);
335 swap(lhs.gaps_, rhs.gaps_);
336 }
337
338 private:
339 void allocateBuffer() {
340 void* ptr = detail::alignedMallocOrThrow(kBufferSize * sizeof(T), alignment);
341
342 if (buffersPos_ < buffersSize_) {
343 buffers_.load(std::memory_order_acquire)[buffersPos_++] = static_cast<T*>(ptr);
344 } else {
345 const Index oldBuffersSize = buffersSize_;
346 T** oldBuffers = buffers_.load(std::memory_order_acquire);
347
348 buffersSize_ = oldBuffersSize == 0 ? 2 : oldBuffersSize * 2;
349 T** newBuffers = new T*[buffersSize_];
350
351 if (oldBuffers != nullptr) {
352 std::memcpy(newBuffers, oldBuffers, sizeof(T*) * oldBuffersSize);
353 deleteLater_.push_back(oldBuffers);
354 }
355
356 newBuffers[buffersPos_++] = static_cast<T*>(ptr);
357 buffers_.store(newBuffers, std::memory_order_release);
358 }
359 }
360
361 // Constructs [beginIndex, endIndex). built tracks one past the last constructed element, so a
362 // caller can destroy exactly those if a constructor throws.
363 void constructObjects(const Index beginIndex, const Index endIndex, Index& built) {
364 const Index startBuffer = beginIndex >> kLog2BuffSize;
365 const Index endBuffer = endIndex >> kLog2BuffSize;
366
367 Index bufStart = beginIndex & kMask;
368 for (Index b = startBuffer; b <= endBuffer; ++b) {
369 T* buf = buffers_.load(std::memory_order_acquire)[b];
370 const Index bufEnd = b == endBuffer ? (endIndex & kMask) : kBufferSize;
371 for (Index i = bufStart; i < bufEnd; ++i) {
372 new (buf + i) T();
373 built = (b << kLog2BuffSize) + i + 1;
374 }
375
376 bufStart = 0;
377 }
378 }
379
380 void destroyRange(T** buffers, Index beginIndex, const Index endIndex) {
381 for (; beginIndex < endIndex; ++beginIndex) {
382 buffers[beginIndex >> kLog2BuffSize][beginIndex & kMask].~T();
383 }
384 }
385
386 // Destroys every element, skipping gaps left by constructors that threw. Not concurrency safe.
387 void destroyObjects(T** buffers) {
388 if (std::is_trivially_destructible<T>::value || buffers == nullptr) {
389 return;
390 }
391 std::sort(gaps_.begin(), gaps_.end());
392 const Index end = pos_.load(std::memory_order_relaxed);
393 Index next = 0;
394 for (const std::pair<Index, Index>& gap : gaps_) {
395 destroyRange(buffers, next, gap.first);
396 next = gap.second;
397 }
398 destroyRange(buffers, next, end);
399 }
400
401 // Records [beginIndex, endIndex), reserved by a grow_by() whose constructor threw, as holding no
402 // objects.
403 void recordGap(const Index beginIndex, const Index endIndex) {
404 const std::lock_guard<std::mutex> guard(resizeMutex_);
405 gaps_.emplace_back(beginIndex, endIndex);
406 }
407 //
408 // kBufferSize = 2^kLog2BuffSize
409 // mask = 0b00011111
410 // ──┬──
411 // └─number of 1s is log2BuffSize
412 //
413 std::mutex resizeMutex_;
414
415 Index kLog2BuffSize;
416 Index kBufferSize;
417 Index kMask;
418
419 std::atomic<Index> pos_;
420 std::atomic<Index> allocatedSize_;
421
422 std::atomic<T**> buffers_;
423 Index buffersSize_;
424 Index buffersPos_;
425 std::vector<T**> deleteLater_;
426 // Index ranges reserved by a grow_by() whose constructor threw. They hold no objects.
427 std::vector<std::pair<Index, Index>> gaps_;
428};
429
430} // namespace dispenso
Index getBufferSize(const Index index) const
const T * getBuffer(const Index index) const
ConcurrentObjectArena(const Index minBuffSize, const Index initialSize=0)
ConcurrentObjectArena(ConcurrentObjectArena< T, Index, alignment > &&other) noexcept
ConcurrentObjectArena(const ConcurrentObjectArena< T, Index, alignment > &other)
uint32_t log2(uint64_t v)
Compute log base 2 of a value (runtime).
Definition util.h:193