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 ALICEO2_ITSMFT_TRACKING_SLABBUMPALLOCATOR_H_
17#define ALICEO2_ITSMFT_TRACKING_SLABBUMPALLOCATOR_H_
18
19#include <algorithm>
20#include <atomic>
21#include <cassert>
22#include <cstddef>
23#include <cstdint>
24#include <memory>
25#include <memory_resource>
26#include <new>
27#include <numeric>
28#include <stdexcept>
29#include <utility>
30#include <vector>
31
33
35{
36
37namespace detail
38{
39
41{
42 public:
43 using Factory = void* (*)(void*);
44 using Deleter = void (*)(void*);
45
46 ThreadLocalStorage(void* context, Factory factory, Deleter deleter);
50
51 void* local();
52 std::vector<void*> values() const;
53
54 private:
55 struct Impl;
56 std::unique_ptr<Impl> mImpl;
57};
58
59using ParallelForBody = void (*)(void*, size_t, size_t);
60void parallelFor(size_t begin, size_t end, size_t grainSize, void* context, ParallelForBody body);
61
62template <typename T>
68
69} // namespace detail
70
72{
73 public:
74 struct Range {
75 size_t base{0};
76 size_t n{0};
77 bool valid() const noexcept { return n != 0; }
78 };
79
80 SlabBumpAllocator(size_t capacity, size_t slab) noexcept;
81
82 Range grab() noexcept;
83
84 [[nodiscard]] size_t capacity() const noexcept { return mCapacity; }
85 [[nodiscard]] size_t slab() const noexcept { return mSlab; }
86 [[nodiscard]] size_t watermark() const noexcept;
87
88 static size_t suggestSlab(size_t capacity, int nThreads, size_t minSlab = 256, size_t maxSlab = 4096) noexcept;
89
90 void resetCapacity(size_t capacity) noexcept;
91
92 private:
93 std::atomic<size_t> mCursor{0};
94 std::atomic<bool> mExhausted{false};
95 size_t mCapacity;
96 size_t mSlab;
97};
98
99enum class SlabMode : uint8_t {
100 Unordered,
102};
103
105 size_t requested{0};
106 size_t capacity{0};
107 size_t emitted{0};
108 size_t spilled{0};
109 bool overflowed{false};
110 bool memoryLimited{false};
111};
112
113template <typename T, SlabMode Mode>
115{
116 static constexpr int32_t NoProducer = -1;
117
118 public:
119 struct Config {
120 size_t capacity{0};
121 int nThreads{1};
123 size_t slabOverride{0};
124 };
125
126 static constexpr size_t BytesPerSlot = Mode == SlabMode::GroupedByProducer ? (2 * sizeof(T)) + sizeof(int32_t) : sizeof(T);
127
128 struct Run {
129 size_t begin{0};
130 size_t end{0};
131 };
132
133 class Handle
134 {
135 public:
136 explicit Handle(SlabSink* sink)
137 : mSink{sink}, mRuns{sink->memoryResource()}, mSpill{sink->memoryResource()}, mSpillProducer{sink->memoryResource()} {}
138
139 void beginProducer(int32_t p) noexcept { mProducer = p; }
140
141 template <typename... Args>
142 void emplace(Args&&... args)
143 {
144 if constexpr (Mode == SlabMode::GroupedByProducer) {
145 assert(mProducer != NoProducer);
146 }
147 ++mEmitted;
148 if (mSlot == mSlotEnd && !refill()) {
149 mSpill.emplace_back(std::forward<Args>(args)...);
150 if constexpr (Mode == SlabMode::GroupedByProducer) {
151 mSpillProducer.push_back(mProducer);
152 }
153 return;
154 }
155 mSink->store(mSlot++, mProducer, std::forward<Args>(args)...);
156 }
157
158 [[nodiscard]] size_t emitted() const noexcept { return mEmitted; }
159 [[nodiscard]] size_t spilled() const noexcept { return mSpill.size(); }
160
161 private:
162 friend class SlabSink;
163
164 bool refill()
165 {
166 if (mDrained) { // the arena is gone, do not touch the shared cursor again
167 return false;
168 }
169 closeRun();
170 const auto r = mSink->mAlloc.grab();
171 if (!r.valid()) {
172 mDrained = true;
173 return false;
174 }
175 mRunBegin = r.base;
176 mSlot = r.base;
177 mSlotEnd = r.base + r.n;
178 return true;
179 }
180
181 void closeRun()
182 {
183 if constexpr (Mode == SlabMode::Unordered) {
184 if (mSlot > mRunBegin) {
185 mRuns.push_back(Run{.begin = mRunBegin, .end = mSlot});
186 mRunBegin = mSlot; // only advanced once push_back succeeded, so a throw can be retried
187 }
188 }
189 }
190
191 SlabSink* mSink{nullptr};
192 size_t mSlot{0};
193 size_t mSlotEnd{0};
194 size_t mRunBegin{0};
195 int32_t mProducer{NoProducer};
196 bool mDrained{false};
197 size_t mEmitted{0};
198 bounded_vector<Run> mRuns;
199 bounded_vector<T> mSpill;
200 bounded_vector<int32_t> mSpillProducer;
201 };
202
204 : SlabSink{cfg, grantedCapacity(cfg.capacity, cfg.nConcurrentSinks, mr), mr} {}
205
206 SlabSink(SlabSink&&) = delete;
207 SlabSink(const SlabSink&) = delete;
209 SlabSink& operator=(const SlabSink&) = delete;
210 ~SlabSink() = default;
211
212 Handle& local() { return *static_cast<Handle*>(mHandles.local()); }
213
214 [[nodiscard]] std::pmr::memory_resource* memoryResource() const noexcept { return mMR; }
215
216 [[nodiscard]] SlabSinkStats stats() const
217 {
219 s.requested = mRequested;
220 s.capacity = mAlloc.capacity();
221 s.memoryLimited = s.capacity < s.requested;
222 for (const void* value : mHandles.values()) {
223 const auto& h = *static_cast<const Handle*>(value);
224 s.emitted += h.emitted();
225 s.spilled += h.spilled();
226 }
227 s.overflowed = s.spilled != 0;
228 return s;
229 }
230
232 {
233 static_assert(Mode == SlabMode::Unordered);
234 assert(!mFinalized);
235 assert(dest.get_allocator().resource()->is_equal(*mMR));
236 mFinalized = true;
237
238 bounded_vector<Run> runs{mMR};
239 size_t nRuns{0};
240 const auto handles = mHandles.values();
241 for (void* value : handles) {
242 auto& h = *static_cast<Handle*>(value);
243 h.closeRun();
244 nRuns += h.mRuns.size();
245 }
246 runs.reserve(nRuns);
247 for (const void* value : handles) {
248 const auto& h = *static_cast<const Handle*>(value);
249 runs.insert(runs.end(), h.mRuns.begin(), h.mRuns.end());
250 }
251 std::sort(runs.begin(), runs.end(), [](const Run& a, const Run& b) { return a.begin < b.begin; });
252
253 // Runs are disjoint and now ordered, so the compaction target never runs ahead of the source.
254 size_t outputSize{0};
255 for (const auto& run : runs) {
256 for (size_t slot{run.begin}; slot < run.end; ++slot) {
257 if (outputSize != slot) {
258 mStaging[outputSize] = std::move(mStaging[slot]);
259 }
260 ++outputSize;
261 }
262 }
263 deepVectorClear(runs, mMR);
264 mStaging.resize(outputSize);
265 dest.swap(mStaging);
266
267 for (void* value : handles) {
268 auto& h = *static_cast<Handle*>(value);
269 dest.insert(dest.end(), std::make_move_iterator(h.mSpill.begin()), std::make_move_iterator(h.mSpill.end()));
270 deepVectorClear(h.mSpill, mMR);
271 }
272 shrinkIfWasteful(dest);
273 deepVectorClear(mStaging, mMR);
274 }
275
276 void finalizeGrouped(size_t nProducers, bounded_vector<int>& lut, bounded_vector<T>& dest)
277 {
278 static_assert(Mode == SlabMode::GroupedByProducer);
279 assert(!mFinalized);
280 mFinalized = true;
281 const size_t wm = mAlloc.watermark();
282
283 lut.assign(nProducers + 1, 0);
284
285 for (size_t s = 0; s < wm; ++s) {
286 const int32_t p = mProducerOf[s];
287 if (p != NoProducer) {
288 ++lut[p + 1];
289 }
290 }
291 const auto handles = mHandles.values();
292 for (const void* value : handles) {
293 const auto& h = *static_cast<const Handle*>(value);
294 for (const int32_t p : h.mSpillProducer) {
295 ++lut[p + 1];
296 }
297 }
298 std::inclusive_scan(lut.begin(), lut.end(), lut.begin());
299
300 bounded_vector<int> cursor(lut.begin(), lut.begin() + static_cast<ptrdiff_t>(nProducers), mMR);
301 for (size_t s = 0; s < wm; ++s) {
302 const int32_t p = mProducerOf[s];
303 mProducerOf[s] = (p != NoProducer) ? cursor[p]++ : -1;
304 }
305
306 const auto total = static_cast<size_t>(lut.back());
307 dest.resize(total);
308 for (void* value : handles) {
309 auto& h = *static_cast<Handle*>(value);
310 for (size_t i = 0; i < h.mSpill.size(); ++i) {
311 dest[cursor[h.mSpillProducer[i]]++] = std::move(h.mSpill[i]);
312 }
313 deepVectorClear(h.mSpill, mMR);
314 deepVectorClear(h.mSpillProducer, mMR);
315 }
316 deepVectorClear(cursor, mMR);
317
318 detail::MoveContext<T> context{mStaging.data(), mProducerOf.data(), &dest};
319 detail::parallelFor(0, wm, 4096, &context, [](void* opaque, size_t begin, size_t end) {
320 auto& ctx = *static_cast<detail::MoveContext<T>*>(opaque);
321 for (size_t s = begin; s != end; ++s) {
322 const int d = ctx.producerOf[s];
323 if (d < 0) {
324 continue;
325 }
326 (*ctx.destination)[d] = std::move(ctx.staging[s]);
327 }
328 });
329
330 deepVectorClear(mStaging, mMR);
331 deepVectorClear(mProducerOf, mMR);
332 }
333
334 private:
335 SlabSink(const Config& cfg, size_t granted, std::pmr::memory_resource* mr)
336 : mMR{mr},
337 mRequested{cfg.capacity},
338 mAlloc{granted, cfg.slabOverride ? cfg.slabOverride : SlabBumpAllocator::suggestSlab(granted, cfg.nThreads)},
339 mStaging{mr},
340 mProducerOf{mr},
341 mHandles{this, &SlabSink::createHandle, &SlabSink::deleteHandle}
342 {
343 try {
344 mStaging.resize(granted);
345 if constexpr (Mode == SlabMode::GroupedByProducer) {
346 mProducerOf.assign(granted, NoProducer);
347 }
348 } catch (const std::bad_alloc&) {
349 discardPreallocation();
350 } catch (const std::length_error&) {
351 discardPreallocation();
352 }
353 }
354
355 static size_t grantedCapacity(size_t requested, int nConcurrentSinks, const std::pmr::memory_resource* mr) noexcept
356 {
357 const auto* bounded = dynamic_cast<const BoundedMemoryResource*>(mr);
358 if (bounded == nullptr) {
359 return requested;
360 }
361 const size_t used = bounded->getUsedMemory();
362 const size_t limit = bounded->getMaxMemory();
363 const size_t remaining = used < limit ? limit - used : 0;
364 // Keep half of what is left for the spill vectors and whatever else is still live, then
365 // split the rest between the sinks that may be running on this pool at the same time.
366 const size_t budget = (remaining / 2) / static_cast<size_t>(std::max(1, nConcurrentSinks));
367 return std::min(requested, budget / BytesPerSlot);
368 }
369
370 static void shrinkIfWasteful(bounded_vector<T>& v)
371 {
372 if (v.capacity() > v.size() + (v.size() / 4)) {
373 v.shrink_to_fit();
374 }
375 }
376
377 void discardPreallocation()
378 {
379 // Capacity prediction is only an optimization; spilling preserves the output.
380 deepVectorClear(mStaging, mMR);
381 deepVectorClear(mProducerOf, mMR);
382 mAlloc.resetCapacity(0);
383 }
384
385 template <typename... Args>
386 void store(size_t slot, [[maybe_unused]] int32_t producer, Args&&... args)
387 {
388 mStaging[slot] = T(std::forward<Args>(args)...);
389 if constexpr (Mode == SlabMode::GroupedByProducer) {
390 mProducerOf[slot] = producer;
391 }
392 }
393
394 static void* createHandle(void* sink)
395 {
396 return new Handle{static_cast<SlabSink*>(sink)};
397 }
398
399 static void deleteHandle(void* handle)
400 {
401 delete static_cast<Handle*>(handle);
402 }
403
404 std::pmr::memory_resource* mMR{nullptr};
405 size_t mRequested{0};
406 SlabBumpAllocator mAlloc;
407 bounded_vector<T> mStaging;
408 bounded_vector<int32_t> mProducerOf;
409 detail::ThreadLocalStorage mHandles;
410 bool mFinalized{false};
411};
412
413template <typename T>
415
416template <typename T>
418
419} // namespace o2::itsmft::tracking
420
421#endif /* ALICEO2_ITSMFT_TRACKING_SLABBUMPALLOCATOR_H_ */
int32_t i
Mode
Definition Utils.h:89
Class for time synchronization of RawReader instances.
void resetCapacity(size_t capacity) noexcept
static size_t suggestSlab(size_t capacity, int nThreads, size_t minSlab=256, size_t maxSlab=4096) noexcept
SlabSink(const Config &cfg, std::pmr::memory_resource *mr)
static constexpr size_t BytesPerSlot
void finalizeGrouped(size_t nProducers, bounded_vector< int > &lut, bounded_vector< T > &dest)
SlabSink(SlabSink &&)=delete
std::pmr::memory_resource * memoryResource() const noexcept
void finalizeUnordered(bounded_vector< T > &dest)
SlabSink(const SlabSink &)=delete
SlabSink & operator=(const SlabSink &)=delete
SlabSink & operator=(SlabSink &&)=delete
ThreadLocalStorage(const ThreadLocalStorage &)=delete
ThreadLocalStorage & operator=(const ThreadLocalStorage &)=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
GLsizei const GLfloat * value
Definition glcorearb.h:819
typedef void(APIENTRYP PFNGLCULLFACEPROC)(GLenum mode)
GLboolean r
Definition glcorearb.h:1233
GLboolean GLboolean GLboolean GLboolean a
Definition glcorearb.h:1233
void parallelFor(size_t begin, size_t end, size_t grainSize, void *context, ParallelForBody body)
void(*)(void *, size_t, size_t) ParallelForBody
void deepVectorClear(std::vector< T > &vec)
std::pmr::vector< T > bounded_vector
size_t capacity
slots the memory pool actually granted
size_t requested
slots the caller predicted it would need
bool memoryLimited
the pool granted less than was requested
bool overflowed
something did not fit into the staging area
int nConcurrentSinks
sinks that may be alive on the same pool at the same time
size_t capacity
predicted number of slots
size_t slabOverride
0: derive the slab size from the granted capacity
int nThreads
workers that will feed this sink