Project
Loading...
Searching...
No Matches
SlabBumpAllocator.h
Go to the documentation of this file.
1// Copyright 2019-2026 CERN and copyright holders of ALICE O2.
2// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders.
3// All rights not expressly granted are reserved.
4//
5// This software is distributed under the terms of the GNU General Public
6// License v3 (GPL Version 3), copied verbatim in the file "COPYING".
7//
8// In applying this license CERN does not waive the privileges and immunities
9// granted to it by virtue of its status as an Intergovernmental Organization
10// or submit itself to any jurisdiction.
15
16#ifndef TRACKINGITSU_INCLUDE_SLABBUMPALLOCATOR_H_
17#define TRACKINGITSU_INCLUDE_SLABBUMPALLOCATOR_H_
18
19#include <algorithm>
20#include <atomic>
21#include <cassert>
22#include <cstddef>
23#include <cstdint>
24#include <memory_resource>
25#include <new>
26#include <numeric>
27#include <stdexcept>
28#include <utility>
29
30#include <oneapi/tbb/blocked_range.h>
31#include <oneapi/tbb/enumerable_thread_specific.h>
32#include <oneapi/tbb/parallel_for.h>
33
35
36namespace o2::its
37{
38
40{
41 public:
42 struct Range {
43 size_t base{0};
44 size_t n{0};
45 bool valid() const noexcept { return n != 0; }
46 };
47
48 SlabBumpAllocator(size_t capacity, size_t slab) noexcept
49 : mCapacity{capacity}, mSlab{slab ? slab : size_t{1}} {}
50
51 Range grab() noexcept
52 {
53 if (mExhausted.load(std::memory_order_relaxed)) {
54 return {};
55 }
56 const size_t base = mCursor.fetch_add(mSlab, std::memory_order_relaxed);
57 if (base >= mCapacity) {
58 mExhausted.store(true, std::memory_order_relaxed);
59 return {};
60 }
61 return {.base = base, .n = std::min(mSlab, mCapacity - base)};
62 }
63
64 [[nodiscard]] size_t capacity() const noexcept { return mCapacity; }
65 [[nodiscard]] size_t slab() const noexcept { return mSlab; }
66 [[nodiscard]] size_t watermark() const noexcept
67 {
68 return std::min(mCursor.load(std::memory_order_relaxed), mCapacity);
69 }
70
71 static size_t suggestSlab(size_t capacity, int nThreads, size_t minSlab = 256, size_t maxSlab = 4096) noexcept
72 {
73 const size_t t = static_cast<size_t>(std::max(1, nThreads));
74 const size_t fairShare = std::max<size_t>(1, capacity / t);
75 return std::clamp(std::max<size_t>(1, capacity / (8 * t)),
76 std::min(minSlab, fairShare),
77 std::min(maxSlab, fairShare));
78 }
79
80 void resetCapacity(size_t capacity) noexcept
81 {
82 assert(mCursor.load(std::memory_order_relaxed) == 0);
83 mCapacity = capacity;
84 mExhausted.store(capacity == 0, std::memory_order_relaxed);
85 }
86
87 private:
88 std::atomic<size_t> mCursor{0};
89 std::atomic<bool> mExhausted{false};
90 size_t mCapacity;
91 size_t mSlab;
92};
93
94enum class SlabMode : uint8_t {
97};
98
100 size_t requested{0};
101 size_t capacity{0};
102 size_t emitted{0};
103 size_t spilled{0};
104 bool overflowed{false};
105 bool memoryLimited{false};
106};
107
108template <typename T, SlabMode Mode>
110{
111 static constexpr int32_t NoProducer = -1;
112
113 public:
114 struct Config {
115 size_t capacity{0};
116 int nThreads{1};
118 size_t slabOverride{0};
119 };
120
121 static constexpr size_t BytesPerSlot = Mode == SlabMode::GroupedByProducer ? (2 * sizeof(T)) + sizeof(int32_t) : sizeof(T);
122
123 struct Run {
124 size_t begin{0};
125 size_t end{0};
126 };
127
128 class Handle
129 {
130 public:
131 explicit Handle(SlabSink* sink)
132 : mSink{sink}, mRuns{sink->memoryResource()}, mSpill{sink->memoryResource()}, mSpillProducer{sink->memoryResource()} {}
133
134 void beginProducer(int32_t p) noexcept { mProducer = p; }
135
136 template <typename... Args>
137 void emplace(Args&&... args)
138 {
139 if constexpr (Mode == SlabMode::GroupedByProducer) {
140 assert(mProducer != NoProducer);
141 }
142 ++mEmitted;
143 if (mSlot == mSlotEnd && !refill()) {
144 mSpill.emplace_back(std::forward<Args>(args)...);
145 if constexpr (Mode == SlabMode::GroupedByProducer) {
146 mSpillProducer.push_back(mProducer);
147 }
148 return;
149 }
150 mSink->store(mSlot++, mProducer, std::forward<Args>(args)...);
151 }
152
153 [[nodiscard]] size_t emitted() const noexcept { return mEmitted; }
154 [[nodiscard]] size_t spilled() const noexcept { return mSpill.size(); }
155
156 private:
157 friend class SlabSink;
158
159 bool refill()
160 {
161 if (mDrained) { // the arena is gone, do not touch the shared cursor again
162 return false;
163 }
164 closeRun();
165 const auto r = mSink->mAlloc.grab();
166 if (!r.valid()) {
167 mDrained = true;
168 return false;
169 }
170 mRunBegin = r.base;
171 mSlot = r.base;
172 mSlotEnd = r.base + r.n;
173 return true;
174 }
175
176 void closeRun()
177 {
178 if constexpr (Mode == SlabMode::Unordered) {
179 if (mSlot > mRunBegin) {
180 mRuns.push_back(Run{.begin = mRunBegin, .end = mSlot});
181 mRunBegin = mSlot; // only advanced once push_back succeeded, so a throw can be retried
182 }
183 }
184 }
185
186 SlabSink* mSink{nullptr};
187 size_t mSlot{0};
188 size_t mSlotEnd{0};
189 size_t mRunBegin{0};
190 int32_t mProducer{NoProducer};
191 bool mDrained{false};
192 size_t mEmitted{0};
193 bounded_vector<Run> mRuns;
194 bounded_vector<T> mSpill;
195 bounded_vector<int32_t> mSpillProducer;
196 };
197
199 : SlabSink{cfg, grantedCapacity(cfg.capacity, cfg.nConcurrentSinks, mr), mr} {}
200
201 SlabSink(SlabSink&&) = delete;
202 SlabSink(const SlabSink&) = delete;
204 SlabSink& operator=(const SlabSink&) = delete;
205 ~SlabSink() = default;
206
207 Handle& local() { return mHandles.local(); }
208
209 [[nodiscard]] std::pmr::memory_resource* memoryResource() const noexcept { return mMR; }
210
211 [[nodiscard]] SlabSinkStats stats() const
212 {
214 s.requested = mRequested;
215 s.capacity = mAlloc.capacity();
216 s.memoryLimited = s.capacity < s.requested;
217 for (const auto& h : mHandles) {
218 s.emitted += h.emitted();
219 s.spilled += h.spilled();
220 }
221 s.overflowed = s.spilled != 0;
222 return s;
223 }
224
226 {
227 static_assert(Mode == SlabMode::Unordered);
228 assert(!mFinalized);
229 assert(dest.get_allocator().resource()->is_equal(*mMR));
230 mFinalized = true;
231
232 bounded_vector<Run> runs{mMR};
233 size_t nRuns{0};
234 for (auto& h : mHandles) {
235 h.closeRun();
236 nRuns += h.mRuns.size();
237 }
238 runs.reserve(nRuns);
239 for (const auto& h : mHandles) {
240 runs.insert(runs.end(), h.mRuns.begin(), h.mRuns.end());
241 }
242 std::sort(runs.begin(), runs.end(), [](const Run& a, const Run& b) { return a.begin < b.begin; });
243
244 // Runs are disjoint and now ordered, so the compaction target never runs ahead of the source.
245 size_t outputSize{0};
246 for (const auto& run : runs) {
247 for (size_t slot{run.begin}; slot < run.end; ++slot) {
248 if (outputSize != slot) {
249 mStaging[outputSize] = std::move(mStaging[slot]);
250 }
251 ++outputSize;
252 }
253 }
254 deepVectorClear(runs, mMR);
255 mStaging.resize(outputSize);
256 dest.swap(mStaging);
257
258 for (auto& h : mHandles) {
259 dest.insert(dest.end(), std::make_move_iterator(h.mSpill.begin()), std::make_move_iterator(h.mSpill.end()));
260 deepVectorClear(h.mSpill, mMR);
261 }
262 shrinkIfWasteful(dest);
263 deepVectorClear(mStaging, mMR);
264 }
265
266 void finalizeGrouped(size_t nProducers, bounded_vector<int>& lut, bounded_vector<T>& dest)
267 {
268 static_assert(Mode == SlabMode::GroupedByProducer);
269 assert(!mFinalized);
270 mFinalized = true;
271 const size_t wm = mAlloc.watermark();
272
273 lut.assign(nProducers + 1, 0);
274
275 for (size_t s = 0; s < wm; ++s) {
276 const int32_t p = mProducerOf[s];
277 if (p != NoProducer) {
278 ++lut[p + 1];
279 }
280 }
281 for (const auto& h : mHandles) {
282 for (const int32_t p : h.mSpillProducer) {
283 ++lut[p + 1];
284 }
285 }
286 std::inclusive_scan(lut.begin(), lut.end(), lut.begin());
287
288 bounded_vector<int> cursor(lut.begin(), lut.begin() + static_cast<ptrdiff_t>(nProducers), mMR);
289 for (size_t s = 0; s < wm; ++s) {
290 const int32_t p = mProducerOf[s];
291 mProducerOf[s] = (p != NoProducer) ? cursor[p]++ : -1;
292 }
293
294 const auto total = static_cast<size_t>(lut.back());
295 dest.resize(total);
296 for (auto& h : mHandles) {
297 for (size_t i = 0; i < h.mSpill.size(); ++i) {
298 dest[cursor[h.mSpillProducer[i]]++] = std::move(h.mSpill[i]);
299 }
300 deepVectorClear(h.mSpill, mMR);
301 deepVectorClear(h.mSpillProducer, mMR);
302 }
303 deepVectorClear(cursor, mMR);
304
305 T* const staging = mStaging.data();
306 tbb::parallel_for(tbb::blocked_range<size_t>(0, wm, 4096), [&](const tbb::blocked_range<size_t>& r) {
307 for (size_t s = r.begin(); s != r.end(); ++s) {
308 const int d = mProducerOf[s];
309 if (d < 0) {
310 continue;
311 }
312 dest[d] = std::move(staging[s]);
313 }
314 });
315
316 deepVectorClear(mStaging, mMR);
317 deepVectorClear(mProducerOf, mMR);
318 }
319
320 private:
321 SlabSink(const Config& cfg, size_t granted, std::pmr::memory_resource* mr)
322 : mMR{mr},
323 mRequested{cfg.capacity},
324 mAlloc{granted, cfg.slabOverride ? cfg.slabOverride : SlabBumpAllocator::suggestSlab(granted, cfg.nThreads)},
325 mStaging{mr},
326 mProducerOf{mr},
327 mHandles{[this]() { return Handle{this}; }}
328 {
329 try {
330 mStaging.resize(granted);
331 if constexpr (Mode == SlabMode::GroupedByProducer) {
332 mProducerOf.assign(granted, NoProducer);
333 }
334 } catch (const std::bad_alloc&) {
335 discardPreallocation();
336 } catch (const std::length_error&) {
337 discardPreallocation();
338 }
339 }
340
341 static size_t grantedCapacity(size_t requested, int nConcurrentSinks, const std::pmr::memory_resource* mr) noexcept
342 {
343 const auto* bounded = dynamic_cast<const BoundedMemoryResource*>(mr);
344 if (bounded == nullptr) {
345 return requested;
346 }
347 const size_t used = bounded->getUsedMemory();
348 const size_t limit = bounded->getMaxMemory();
349 const size_t remaining = used < limit ? limit - used : 0;
350 // Keep half of what is left for the spill vectors and whatever else is still live, then
351 // split the rest between the sinks that may be running on this pool at the same time.
352 const size_t budget = (remaining / 2) / static_cast<size_t>(std::max(1, nConcurrentSinks));
353 return std::min(requested, budget / BytesPerSlot);
354 }
355
356 static void shrinkIfWasteful(bounded_vector<T>& v)
357 {
358 if (v.capacity() > v.size() + (v.size() / 4)) {
359 v.shrink_to_fit();
360 }
361 }
362
363 void discardPreallocation()
364 {
365 // Capacity prediction is only an optimization; spilling preserves the output.
366 deepVectorClear(mStaging, mMR);
367 deepVectorClear(mProducerOf, mMR);
368 mAlloc.resetCapacity(0);
369 }
370
371 template <typename... Args>
372 void store(size_t slot, [[maybe_unused]] int32_t producer, Args&&... args)
373 {
374 mStaging[slot] = T(std::forward<Args>(args)...);
375 if constexpr (Mode == SlabMode::GroupedByProducer) {
376 mProducerOf[slot] = producer;
377 }
378 }
379
380 std::pmr::memory_resource* mMR{nullptr};
381 size_t mRequested{0};
382 SlabBumpAllocator mAlloc;
383 bounded_vector<T> mStaging;
384 bounded_vector<int32_t> mProducerOf;
385 tbb::enumerable_thread_specific<Handle> mHandles;
386 bool mFinalized{false};
387};
388
389template <typename T>
391
392template <typename T>
394
395} // namespace o2::its
396
397#endif /* TRACKINGITSU_INCLUDE_SLABBUMPALLOCATOR_H_ */
int32_t i
Mode
Definition Utils.h:89
Class for time synchronization of RawReader instances.
static size_t suggestSlab(size_t capacity, int nThreads, size_t minSlab=256, size_t maxSlab=4096) noexcept
size_t slab() const noexcept
SlabBumpAllocator(size_t capacity, size_t slab) noexcept
void resetCapacity(size_t capacity) noexcept
size_t capacity() const noexcept
size_t watermark() const noexcept
size_t emitted() const noexcept
void beginProducer(int32_t p) noexcept
size_t spilled() const noexcept
void emplace(Args &&... args)
SlabSink(SlabSink &&)=delete
~SlabSink()=default
SlabSink(const SlabSink &)=delete
static constexpr size_t BytesPerSlot
void finalizeGrouped(size_t nProducers, bounded_vector< int > &lut, bounded_vector< T > &dest)
SlabSink & operator=(SlabSink &&)=delete
SlabSink(const Config &cfg, std::pmr::memory_resource *mr)
void finalizeUnordered(bounded_vector< T > &dest)
SlabSinkStats stats() const
std::pmr::memory_resource * memoryResource() const noexcept
SlabSink & operator=(const SlabSink &)=delete
GLdouble n
Definition glcorearb.h:1982
GLuint GLuint end
Definition glcorearb.h:469
const GLdouble * v
Definition glcorearb.h:832
GLboolean GLboolean GLboolean b
Definition glcorearb.h:1233
GLboolean r
Definition glcorearb.h:1233
GLboolean GLboolean GLboolean GLboolean a
Definition glcorearb.h:1233
void deepVectorClear(std::vector< T > &vec)
std::pmr::vector< T > bounded_vector
size_t requested
slots the caller predicted it would need
bool overflowed
something did not fit into the staging area
bool memoryLimited
the pool granted less than was requested
size_t capacity
slots the memory pool actually granted
size_t capacity
predicted number of slots
size_t slabOverride
0: derive the slab size from the granted capacity
int nConcurrentSinks
sinks that may be alive on the same pool at the same time
int nThreads
workers that will feed this sink