dispenso 1.6.2
A library for task parallelism
Loading...
Searching...
No Matches
concurrent_vector.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
8#pragma once
9
65#include <algorithm>
66#include <cassert>
67#include <climits>
68#include <cstring>
69#include <initializer_list>
70#include <iterator>
71#include <stdexcept>
72#include <utility>
73#include <vector>
74
75#include <dispenso/detail/math.h>
76#include <dispenso/platform.h>
78
79// Whether to use a non-atomic buffer pointer cache for read-hot paths.
80// Disabled on ARM where cache-line writes on every growth operation cause store buffer
81// pressure that exceeds the read-path benefit (acquire loads are effectively free for
82// sequential access patterns on ARM).
83#if !defined(DISPENSO_HAS_CACHED_PTRS)
84#if !defined(__aarch64__) && !defined(_M_ARM64)
85#define DISPENSO_HAS_CACHED_PTRS 1
86#else
87#define DISPENSO_HAS_CACHED_PTRS 0
88#endif
89#endif
90
91namespace dispenso {
92
104enum class ConcurrentVectorReallocStrategy { kFullBufferAhead, kHalfBufferAhead, kAsNeeded };
105
106struct ReserveTagS {};
112
113// Textual inclusion. Includes undocumented implementation details, e.g. iterators.
114#include <dispenso/detail/concurrent_vector_impl.h>
115
120template <typename T>
126 static constexpr size_t kDefaultCapacity = (sizeof(T) >= 256) ? 2 : 512 / sizeof(T);
127
135 static constexpr size_t kMaxVectorSize =
136 (size_t{1} << (sizeof(size_t) * CHAR_BIT > 47 ? 47 : sizeof(size_t) * CHAR_BIT - 1)) /
137 sizeof(T);
138};
139
152 static constexpr bool kPreferBuffersInline = true;
153
165 ConcurrentVectorReallocStrategy::kAsNeeded;
166
176 static constexpr bool kIteratorPreferSpeed = true;
177};
178
184template <
185 typename T,
186 typename Traits = DefaultConcurrentVectorTraits,
187 typename SizeTraits = DefaultConcurrentVectorSizeTraits<T>>
189 public:
190 using value_type = T;
191 using reference = T&;
192 using const_reference = const T&;
193 using size_type = size_t;
194 using difference_type = ssize_t;
195 using reference_type = T&;
196 using const_reference_type = const T&;
197 using pointer = T*;
198 using const_pointer = const T*;
199 using iterator = std::conditional_t<
200 Traits::kIteratorPreferSpeed,
201 cv::ConcurrentVectorIterator<ConcurrentVector<T, Traits>, T, false>,
202 cv::CompactCVecIterator<ConcurrentVector<T, Traits>, T, false>>;
203 using const_iterator = std::conditional_t<
204 Traits::kIteratorPreferSpeed,
205 cv::ConcurrentVectorIterator<ConcurrentVector<T, Traits>, T, true>,
206 cv::CompactCVecIterator<ConcurrentVector<T, Traits>, T, true>>;
207 using reverse_iterator = std::reverse_iterator<iterator>;
208 using const_reverse_iterator = std::reverse_iterator<const_iterator>;
209
213 ConcurrentVector() : ConcurrentVector(SizeTraits::kDefaultCapacity / 2, ReserveTag) {}
214
220 ConcurrentVector(size_t startCapacity, ReserveTagS)
221 : firstBucketShift_(
222 detail::log2(
223 detail::nextPow2(std::max(startCapacity, SizeTraits::kDefaultCapacity / 2)))),
224 firstBucketLen_(size_type{1} << firstBucketShift_) {
225 T* firstTwo = cv::alloc<T>(2 * firstBucketLen_);
226 initCachedPtrs();
227 setCachedPtr(0, firstTwo);
228 setCachedPtr(1, firstTwo + firstBucketLen_);
229 buffers_[0].store(firstTwo, std::memory_order_release);
230 buffers_[1].store(firstTwo + firstBucketLen_, std::memory_order_release);
231 }
232
236 explicit ConcurrentVector(size_t startSize) : ConcurrentVector(startSize, ReserveTag) {
237 T* buf = buffers_[0].load(std::memory_order_relaxed);
238 SizeOnExit constructed{size_, 0};
239 for (; constructed.count < startSize; ++constructed.count) {
240 new (buf + constructed.count) T();
241 }
242 }
243
247 ConcurrentVector(size_t startSize, const T& defaultValue)
248 : ConcurrentVector(startSize, ReserveTag) {
249 T* buf = buffers_[0].load(std::memory_order_relaxed);
250 SizeOnExit constructed{size_, 0};
251 for (; constructed.count < startSize; ++constructed.count) {
252 new (buf + constructed.count) T(defaultValue);
253 }
254 }
255
259 template <typename InIterator, typename = cv::EnableIfInputIterator<InIterator>>
260 ConcurrentVector(InIterator start, InIterator end)
261 : ConcurrentVector(start, end, cv::IteratorCategory<InIterator>{}) {}
262
268 template <typename InIterator, typename = cv::EnableIfInputIterator<InIterator>>
269 ConcurrentVector(size_type startSize, InIterator start, InIterator end)
270 : ConcurrentVector(startSize, ReserveTag) {
271 assert(cv::rangeLengthIs(start, end, startSize, cv::IteratorCategory<InIterator>{}));
272 initFromEmpty(start, end);
273 }
274
278 ConcurrentVector(std::initializer_list<T> l)
279 : ConcurrentVector(l.size(), std::begin(l), std::end(l)) {}
280
285 : ConcurrentVector(other.size(), other.cbegin(), other.cend()) {}
286
291 : buffers_(std::move(other.buffers_)),
292 firstBucketShift_(other.firstBucketShift_),
293 firstBucketLen_(other.firstBucketLen_),
294 gaps_(other.gaps_.exchange(nullptr, std::memory_order_relaxed)),
295 size_(other.size_.load(std::memory_order_relaxed)) {
296 moveCachedPtrsFrom(other);
297 other.size_.store(0, std::memory_order_relaxed);
298 // This is possibly unnecessary overhead, but enables the "other" vector to be in a valid,
299 // usable state right away, no empty check or clear required, as it is for std::vector.
300 T* firstTwo = cv::alloc<T>(2 * firstBucketLen_);
301 other.setCachedPtr(0, firstTwo);
302 other.setCachedPtr(1, firstTwo + firstBucketLen_);
303 other.buffers_[0].store(firstTwo, std::memory_order_relaxed);
304 other.buffers_[1].store(firstTwo + firstBucketLen_, std::memory_order_relaxed);
305 }
306
310 ConcurrentVector& operator=(const ConcurrentVector& other) {
311 if (&other == this) {
312 return *this;
313 }
314
315 clear();
316 reserve(other.size());
317 initFromEmpty(other.cbegin(), other.cend());
318
319 return *this;
320 }
321
325 ConcurrentVector& operator=(ConcurrentVector&& other) {
326 using std::swap;
327 if (&other == this) {
328 return *this;
329 }
330
331 clear();
332 swap(firstBucketShift_, other.firstBucketShift_);
333 swap(firstBucketLen_, other.firstBucketLen_);
334 buffers_ = std::move(other.buffers_);
335 swapCachedPtrs(other);
336 swapGaps(other);
337 size_t curLen = size_.load(std::memory_order_relaxed);
338 size_.store(other.size_.load(std::memory_order_relaxed), std::memory_order_relaxed);
339 other.size_.store(curLen, std::memory_order_relaxed);
340
341 return *this;
342 }
343
350 void assign(size_type count, const T& value) {
351 clear();
352 reserve(count);
353 SizeOnExit constructed{size_, 0};
354 for (iterator it = begin(); constructed.count < count; ++it, ++constructed.count) {
355 new (&*it) T(value);
356 }
357 }
358
365 template <
366 typename It,
367 typename = typename std::iterator_traits<It>::difference_type,
368 typename = typename std::iterator_traits<It>::pointer,
369 typename = typename std::iterator_traits<It>::reference,
370 typename = typename std::iterator_traits<It>::value_type,
371 typename = typename std::iterator_traits<It>::iterator_category>
372 void assign(It start, It end) {
373 clear();
374 assignRange(start, end, cv::IteratorCategory<It>{});
375 }
376
382 void reserve(difference_type capacity) {
383 auto bend = bucketAndSubIndex(capacity);
384 allocateBufferRange({0, 0, firstBucketLen_}, capacity, bend);
385 }
386
392 void resize(difference_type len) {
393 difference_type curLen = static_cast<difference_type>(size_.load(std::memory_order_relaxed));
394 if (curLen < len) {
395 grow_to_at_least(len);
396 } else if (curLen > len) {
397 auto it = end();
398 auto newEnd = begin() + len;
399 do {
400 --it;
401 it->~T();
402 } while (it != newEnd);
403 size_.store(len, std::memory_order_relaxed);
404 }
405 }
406
413 void resize(difference_type len, const T& value) {
414 difference_type curLen = static_cast<difference_type>(size_.load(std::memory_order_relaxed));
415 if (curLen < len) {
416 grow_to_at_least(len, value);
417 } else if (curLen > len) {
418 auto it = end();
419 auto newEnd = begin() + len;
420 do {
421 --it;
422 it->~T();
423 } while (it != newEnd);
424 size_.store(len, std::memory_order_relaxed);
425 }
426 }
427
432 size_type default_capacity() const {
433 return 2 * firstBucketLen_;
434 }
435
440 size_type capacity() const {
441 size_t cap = 2 * firstBucketLen_;
442 for (size_t b = 2; b < kMaxBuffers; ++b) {
443 if (!buffers_[b].load(std::memory_order_relaxed)) {
444 break;
445 }
446 cap *= 2;
447 }
448 return cap;
449 }
450
455 void clear() {
456#if defined(__cpp_exceptions)
457 if (DISPENSO_EXPECT(gaps_.load(std::memory_order_relaxed) != nullptr, false)) {
458 clearWithGaps();
459 return;
460 }
461#endif // __cpp_exceptions
462 auto binfo = bucketAndSubIndex(size_.load(std::memory_order_relaxed));
463 size_t len = binfo.bucketIndex;
464 size_t cap = binfo.bucketCapacity;
465 size_t b = binfo.bucket;
466 do {
467 T* buf = buffers_[b].load(std::memory_order_relaxed);
468 T* t = buf + len;
469
470 while (t != buf) {
471 --t;
472 t->~T();
473 }
474 cap >>= int{b > 1};
475 len = cap;
476 } while (b--);
477 size_.store(0, std::memory_order_release);
478 }
479
485 constexpr size_t kMaxExtra = 2;
486 auto binfo = bucketAndSubIndex(size_.load(std::memory_order_relaxed));
487
488 // We need to at least skip the first two buckets, since we have those as a single allocation.
489 size_t startBucket = std::max<size_t>(2, binfo.bucket + kMaxExtra);
490
491 for (size_t b = startBucket; b < kMaxBuffers; ++b) {
492 T* ptr = buffers_[b].load(std::memory_order_relaxed);
493 if (!ptr) {
494 break;
495 }
496 if (buffers_.shouldDealloc(b)) {
497 cv::dealloc<T>(ptr);
498 }
499 clearCachedPtr(b);
500 buffers_[b].store(nullptr, std::memory_order_release);
501 }
502 }
503
508 clear();
510 cv::dealloc<T>(buffers_[0].load(std::memory_order_acquire));
511 }
512
519 iterator insert(const_iterator pos, const T& value) {
520 auto it = insertPartial(pos);
521 // insertPartial leaves a live (moved-from or default-constructed) element at pos; assign over
522 // it rather than constructing on top of it, so that element's resources are released.
523 *it = value;
524 return it;
525 }
526
533 iterator insert(const_iterator pos, T&& value) {
534 auto it = insertPartial(pos);
535 *it = std::move(value);
536 return it;
537 }
538
546 iterator insert(const_iterator pos, size_type count, const T& value) {
547 auto it = insertPartial(pos, count);
548 std::fill_n(it, count, value);
549 return it;
550 }
551
559 template <
560 typename InputIt,
561 typename = typename std::iterator_traits<InputIt>::difference_type,
562 typename = typename std::iterator_traits<InputIt>::pointer,
563 typename = typename std::iterator_traits<InputIt>::reference,
564 typename = typename std::iterator_traits<InputIt>::value_type,
565 typename = typename std::iterator_traits<InputIt>::iterator_category>
566 iterator insert(const_iterator pos, InputIt first, InputIt last) {
567 return insertRange(pos, first, last, cv::IteratorCategory<InputIt>{});
568 }
569
576 iterator insert(const_iterator pos, std::initializer_list<T> ilist) {
577 return insert(pos, ilist.begin(), ilist.end());
578 }
579
586 iterator erase(const_iterator pos) {
587 auto e = end();
588 if (e == pos) {
589 return e;
590 }
591 size_.fetch_sub(1, std::memory_order_relaxed);
592 --e;
593 if (e == pos) {
594 e->~T();
595 return e;
596 }
597 auto it = begin();
598 it += (pos - it);
599 it = std::move(pos + 1, const_iterator(e + 1), it);
600 // The last slot was vacated by the shift; destroy the moved-from object left there.
601 e->~T();
602 return it;
603 }
604
613 iterator erase(const_iterator first, const_iterator last) {
614 size_t len = std::distance(first, last);
615 auto it = begin();
616 size_t startIdx = first - it;
617 if (len == 0) {
618 return it + (startIdx + len);
619 }
620 it += startIdx;
621
622 auto e = end();
623 auto e_it = std::move(last, const_iterator(e), it);
624
625 // Destroy the vacated tail [e_it, end()): these are either moved-from or, when the tail was
626 // shorter than the erased range, erased elements that nothing was moved into.
627 while (e != e_it) {
628 --e;
629 e->~T();
630 }
631 size_.fetch_sub(len, std::memory_order_relaxed);
632 return e_it;
633 }
634
640 iterator push_back(const T& val) {
641 return emplace_back(val);
642 }
643
649 iterator push_back(T&& val) {
650 return emplace_back(std::move(val));
651 }
652
658 template <typename... Args>
659 iterator emplace_back(Args&&... args) {
660 auto index = size_.fetch_add(1, std::memory_order_relaxed);
661 auto binfo = bucketAndSubIndex(index);
662
663 allocateBuffer(binfo);
664
665 iterator ret{this, index, binfo};
666
667 T* slot = Traits::kIteratorPreferSpeed ? &*ret : buffers_[binfo.bucket] + binfo.bucketIndex;
668#if defined(__cpp_exceptions)
669 try {
670 new (slot) T(std::forward<Args>(args)...);
671 } catch (...) {
672 finishFailedGrowth(ret, 1);
673 throw;
674 }
675#else
676 new (slot) T(std::forward<Args>(args)...);
677#endif // __cpp_exceptions
678
679 return ret;
680 }
681
689 template <typename Gen>
690 iterator grow_by_generator(size_type delta, Gen gen) {
691 iterator ret = growByUninitialized(delta);
692 constructGrown(ret, delta, [&gen](T* p) { new (p) T(gen()); });
693 return ret;
694 }
695
702 iterator grow_by(size_type delta, const T& t) {
703 iterator ret = growByUninitialized(delta);
704 internalFillN(ret, delta, t);
705 return ret;
706 }
707
713 iterator grow_by(size_type delta) {
714 iterator ret = growByUninitialized(delta);
715 internalFillDefaultN(ret, delta);
716 return ret;
717 }
718
725 template <
726 typename It,
727 typename = typename std::iterator_traits<It>::difference_type,
728 typename = typename std::iterator_traits<It>::pointer,
729 typename = typename std::iterator_traits<It>::reference,
730 typename = typename std::iterator_traits<It>::value_type,
731 typename = typename std::iterator_traits<It>::iterator_category>
732 iterator grow_by(It start, It end) {
733 return growByRange(start, end, cv::IteratorCategory<It>{});
734 }
735
741 iterator grow_by(std::initializer_list<T> initList) {
742 return grow_by(std::begin(initList), std::end(initList));
743 }
744
751 iterator grow_to_at_least(size_type n) {
752 size_t curSize = size_.load(std::memory_order_relaxed);
753 if (curSize < n) {
754 return grow_by(n - curSize);
755 }
756 return {this, n - 1, bucketAndSubIndex(n - 1)};
757 }
758
766 iterator grow_to_at_least(size_type n, const T& t) {
767 size_t curSize = size_.load(std::memory_order_relaxed);
768 if (curSize < n) {
769 return grow_by(n - curSize, t);
770 }
771 return {this, n - 1, bucketAndSubIndex(n - 1)};
772 }
773
777 void pop_back() {
778 auto binfo = bucketAndSubIndex(size_.fetch_sub(1, std::memory_order_relaxed) - 1);
779 T* elt = buffers_[binfo.bucket].load(std::memory_order_relaxed) + binfo.bucketIndex;
780 elt->~T();
781 }
782
791 const T& operator[](size_type index) const {
792 auto binfo = bucketAndSubIndexForIndex(index);
793 T* buf = cachedBuffer(binfo.bucket);
794 return buf[binfo.bucketIndex];
795 }
796
805 T& operator[](size_type index) {
806 auto binfo = bucketAndSubIndexForIndex(index);
807 T* buf = cachedBuffer(binfo.bucket);
808 return buf[binfo.bucketIndex];
809 }
810
820 const T& at(size_type index) const {
821#if defined(__cpp_exceptions)
822 if (index >= size_.load(std::memory_order_relaxed)) {
823 throw std::out_of_range("Index too large");
824 }
825#endif
826 return operator[](index);
827 }
828
838 T& at(size_type index) {
839#if defined(__cpp_exceptions)
840 if (index >= size_.load(std::memory_order_relaxed)) {
841 throw std::out_of_range("Index too large");
842 }
843#endif
844 return operator[](index);
845 }
846
854 iterator begin() {
855 return {this, 0, {0, 0, firstBucketLen_}};
856 }
857
866 iterator end() {
867 size_t curSize = size_.load(std::memory_order_relaxed);
868 auto binfo = bucketAndSubIndex(curSize);
869 return {this, curSize, binfo};
870 }
871
879 const_iterator begin() const {
880 return {this, 0, {0, 0, firstBucketLen_}};
881 }
882
891 const_iterator end() const {
892 size_t curSize = size_.load(std::memory_order_relaxed);
893 auto binfo = bucketAndSubIndex(curSize);
894 return {this, curSize, binfo};
895 }
896
904 const_iterator cbegin() const {
905 return {this, 0, {0, 0, firstBucketLen_}};
906 }
907
916 const_iterator cend() const {
917 size_t curSize = size_.load(std::memory_order_relaxed);
918 auto binfo = bucketAndSubIndex(curSize);
919 return {this, curSize, binfo};
920 }
921
930 reverse_iterator rbegin() {
931 return reverse_iterator(end());
932 }
933
941 reverse_iterator rend() {
942 return reverse_iterator(begin());
943 }
944
953 const_reverse_iterator rbegin() const {
954 return const_reverse_iterator(cend());
955 }
956
964 const_reverse_iterator rend() const {
965 return const_reverse_iterator(cbegin());
966 }
967
973 bool empty() const {
974 return size_.load(std::memory_order_relaxed) == 0;
975 }
976
982 constexpr size_type max_size() const noexcept {
983 return Traits::kMaxVectorSize;
984 }
985
991 size_type size() const {
992 return size_.load(std::memory_order_relaxed);
993 }
994
999 T& front() {
1000 return *buffers_[0].load(std::memory_order_relaxed);
1001 }
1002
1007 const T& front() const {
1008 return *buffers_[0].load(std::memory_order_relaxed);
1009 }
1010
1016 T& back() {
1017 return *(end() - 1);
1018 }
1019
1025 const T& back() const {
1026 return *(end() - 1);
1027 }
1028
1033 void swap(ConcurrentVector& oth) {
1034 using std::swap;
1035 swap(firstBucketShift_, oth.firstBucketShift_);
1036 swap(firstBucketLen_, oth.firstBucketLen_);
1037 size_t othlen = oth.size_.load(std::memory_order_relaxed);
1038 oth.size_.store(size_.load(std::memory_order_relaxed), std::memory_order_relaxed);
1039 size_.store(othlen, std::memory_order_relaxed);
1040
1041 // okay, this relies on the fact that we're essentially swapping in the move operator.
1042 buffers_ = std::move(oth.buffers_);
1043 swapCachedPtrs(oth);
1044 swapGaps(oth);
1045 }
1046
1047 private:
1048 // Range operations dispatch on iterator category, as std::vector does: forward (and stronger)
1049 // iterators can be measured with std::distance up front, but that would consume a single-pass
1050 // input range, so those are consumed exactly once, element by element.
1051 template <typename It>
1052 ConcurrentVector(It start, It end, std::forward_iterator_tag)
1053 : ConcurrentVector(static_cast<size_type>(std::distance(start, end)), start, end) {}
1054
1055 template <typename It>
1056 ConcurrentVector(It start, It end, std::input_iterator_tag) : ConcurrentVector() {
1057 for (; start != end; ++start) {
1058 emplace_back(*start);
1059 }
1060 }
1061
1062 template <typename It>
1063 void assignRange(It start, It end, std::forward_iterator_tag) {
1064 reserve(std::distance(start, end));
1065 initFromEmpty(start, end);
1066 }
1067
1068 template <typename It>
1069 void assignRange(It start, It end, std::input_iterator_tag) {
1070 for (; start != end; ++start) {
1071 emplace_back(*start);
1072 }
1073 }
1074
1075 template <typename It>
1076 iterator insertRange(const_iterator pos, It first, It last, std::forward_iterator_tag) {
1077 size_t len = std::distance(first, last);
1078 auto it = insertPartial(pos, len);
1079 std::copy_n(first, len, it);
1080 return it;
1081 }
1082
1083 template <typename It>
1084 iterator insertRange(const_iterator pos, It first, It last, std::input_iterator_tag) {
1085 // Append, then rotate the new elements into place (as std::vector implementations do).
1086 const difference_type index = pos - cbegin();
1087 const difference_type oldSize = static_cast<difference_type>(size());
1088 for (; first != last; ++first) {
1089 emplace_back(*first);
1090 }
1091 auto b = begin();
1092 std::rotate(b + index, b + oldSize, end());
1093 return b + index;
1094 }
1095
1096 template <typename It>
1097 iterator growByRange(It start, It end, std::forward_iterator_tag) {
1098 const size_t len = static_cast<size_t>(std::distance(start, end));
1099 iterator ret = growByUninitialized(len);
1100 internalInit(start, ret, len);
1101 return ret;
1102 }
1103
1104 template <typename It>
1105 iterator growByRange(It start, It end, std::input_iterator_tag) {
1106 // Only single-pass input iterators come here; forward iterators take the overload above, with
1107 // no copy. grow_by must claim one contiguous block in a single atomic step, so it needs the
1108 // length first, and a single-pass range can be read only once: buffer it to learn the length,
1109 // then move the buffered elements in.
1110 std::vector<T> buffered(start, end);
1111 return growByRange(
1112 std::make_move_iterator(buffered.begin()),
1113 std::make_move_iterator(buffered.end()),
1114 std::random_access_iterator_tag{});
1115 }
1116
1117 DISPENSO_INLINE cv::BucketInfo bucketAndSubIndexForIndex(size_t index) const {
1118#if defined(_MSC_VER) || defined(__aarch64__) || defined(_M_ARM64)
1119 // Branching fast path — benefits MSVC and ARM where branch prediction handles
1120 // sequential access patterns well, avoiding log2/division for the common case.
1121 if (index < firstBucketLen_) {
1122 return {0, index, firstBucketLen_};
1123 }
1124
1125 size_t l2idx = detail::log2(index);
1126 size_t bucket = (l2idx + 1) - firstBucketShift_;
1127 size_t bucketCapacity = size_t{1} << l2idx;
1128 size_t bucketIndex = index - bucketCapacity;
1129
1130 return {bucket, bucketIndex, bucketCapacity};
1131#else
1132 return bucketAndSubIndex(index);
1133#endif // _MSC_VER || __aarch64__ || _M_ARM64
1134 }
1135
1136 DISPENSO_INLINE cv::BucketInfo bucketAndSubIndex(size_t index) const {
1137 size_t l2idx = detail::log2(index | 1);
1138 size_t bucket = (l2idx + 1) - firstBucketShift_;
1139 size_t bucketCapacity = size_t{1} << l2idx;
1140 size_t bucketIndex = index - bucketCapacity;
1141
1142 bucket = index < firstBucketLen_ ? 0 : bucket;
1143 bucketIndex = index < firstBucketLen_ ? index : bucketIndex;
1144 bucketCapacity = index < firstBucketLen_ ? firstBucketLen_ : bucketCapacity;
1145
1146 return {bucket, bucketIndex, bucketCapacity};
1147 }
1148
1149 // Sets size_ to the number of elements constructed so far when it goes out of scope, including
1150 // when a T constructor throws. Used where the vector is filled from empty, not concurrently, so
1151 // that the destructor (or a later clear()) destroys exactly the elements that were constructed.
1152 struct SizeOnExit {
1153 std::atomic<size_t>& size;
1154 size_t count;
1155 ~SizeOnExit() {
1156 size.store(count, std::memory_order_relaxed);
1157 }
1158 };
1159
1160#if defined(__cpp_exceptions)
1161 // A range [first, last) of slots that hold no objects. Gaps form a lock-free, push-only list:
1162 // growth pushes concurrently, and only clear() (not concurrency safe) takes the list down.
1163 struct Gap {
1164 size_t first;
1165 size_t last;
1166 Gap* next;
1167 };
1168
1169 void finishFailedGrowth(const iterator& it, size_t count) {
1170 finishFailedGrowth(it, count, std::is_nothrow_default_constructible<T>{});
1171 }
1172
1173 void finishFailedGrowth(const iterator& first, size_t count, std::true_type) noexcept {
1174 for (iterator it = first; count--; ++it) {
1175 new (&*it) T();
1176 }
1177 }
1178
1179 // Called while an exception propagates; if the gap record cannot be allocated, the program
1180 // terminates.
1181 void finishFailedGrowth(const iterator& it, size_t count, std::false_type) {
1182 const size_t first = static_cast<size_t>(it - begin());
1183 Gap* gap = new Gap{first, first + count, gaps_.load(std::memory_order_relaxed)};
1184 while (!gaps_.compare_exchange_weak(
1185 gap->next, gap, std::memory_order_release, std::memory_order_relaxed)) {
1186 }
1187 }
1188
1189 // clear() for a vector with gaps: destroys every element outside them, last to first, then
1190 // frees the gap records. Allocates nothing.
1191 void clearWithGaps() {
1192 // Insertion-sort the list by descending start; failed growths are rare, so it is short.
1193 Gap* sorted = nullptr;
1194 Gap* gap = gaps_.exchange(nullptr, std::memory_order_acquire);
1195 while (gap) {
1196 Gap* next = gap->next;
1197 Gap** pos = &sorted;
1198 while (*pos && (*pos)->first > gap->first) {
1199 pos = &(*pos)->next;
1200 }
1201 gap->next = *pos;
1202 *pos = gap;
1203 gap = next;
1204 }
1205
1206 size_t index = size_.load(std::memory_order_relaxed);
1207 while (index > 0) {
1208 if (sorted && index == sorted->last) {
1209 index = sorted->first;
1210 Gap* done = sorted;
1211 sorted = sorted->next;
1212 delete done;
1213 continue;
1214 }
1215 --index;
1216 auto binfo = bucketAndSubIndex(index);
1217 (buffers_[binfo.bucket].load(std::memory_order_relaxed) + binfo.bucketIndex)->~T();
1218 }
1219 size_.store(0, std::memory_order_release);
1220 }
1221#else
1222 struct Gap;
1223#endif // __cpp_exceptions
1224
1225 // Constructs the len elements of a range just reserved by growth, starting at it, calling
1226 // construct(T*) for each. If a constructor throws, size_ already covers the whole range and
1227 // other threads may have grown past it, so it cannot shrink: the constructed elements are kept,
1228 // finishFailedGrowth() fills or records the rest, and the exception continues. When construct
1229 // cannot throw, the compiler drops the handler and this is the plain loop.
1230 template <typename Construct>
1231 void constructGrown(iterator it, size_t len, Construct&& construct) {
1232#if defined(__cpp_exceptions)
1233 size_t built = 0;
1234 try {
1235 for (; built < len; ++built, ++it) {
1236 construct(&*it);
1237 }
1238 } catch (...) {
1239 finishFailedGrowth(it, len - built);
1240 throw;
1241 }
1242#else
1243 for (; len--; ++it) {
1244 construct(&*it);
1245 }
1246#endif // __cpp_exceptions
1247 }
1248
1249 void swapGaps(ConcurrentVector& other) {
1250#if defined(__cpp_exceptions)
1251 Gap* mine = gaps_.load(std::memory_order_relaxed);
1252 gaps_.store(other.gaps_.load(std::memory_order_relaxed), std::memory_order_relaxed);
1253 other.gaps_.store(mine, std::memory_order_relaxed);
1254#else
1255 (void)other;
1256#endif // __cpp_exceptions
1257 }
1258
1259 // Copy-construct [start, end) into an empty vector whose capacity is already reserved.
1260 template <typename InIterator>
1261 void initFromEmpty(InIterator start, InIterator end) {
1262 SizeOnExit constructed{size_, 0};
1263 for (iterator it = begin(); start != end; ++it, ++start, ++constructed.count) {
1264 new (&*it) T(*start);
1265 }
1266 }
1267
1268 // The following construct the elements of a range just reserved by growth; see constructGrown.
1269 template <typename InIterator>
1270 void internalInit(InIterator start, iterator it, size_t len) {
1271 constructGrown(it, len, [&start](T* p) {
1272 new (p) T(*start);
1273 ++start;
1274 });
1275 }
1276
1277 void internalFillN(iterator it, size_t len, const T& value) {
1278 constructGrown(it, len, [&value](T* p) { new (p) T(value); });
1279 }
1280
1281 void internalFillDefaultN(iterator it, size_t len) {
1282 constructGrown(it, len, [](T* p) { new (p) T(); });
1283 }
1284
1285 iterator growByUninitialized(size_type delta) {
1286 auto index = size_.fetch_add(delta, std::memory_order_relaxed);
1287 auto binfo = bucketAndSubIndex(index);
1288 auto bend = bucketAndSubIndex(index + delta);
1289 allocateBufferRange(binfo, delta, bend);
1290 return {this, index, binfo};
1291 }
1292
1293 iterator insertPartial(const_iterator pos) {
1294 auto e = end();
1295 auto index = size_.fetch_add(1, std::memory_order_relaxed);
1296 auto binfo = bucketAndSubIndex(index);
1297 allocateBuffer(binfo);
1298 internalFillDefaultN(e, 1);
1299 return std::move_backward(pos, const_iterator(e), e + 1) - 1;
1300 }
1301
1302 iterator insertPartial(const_iterator pos, size_t len) {
1303 auto e = end();
1304 auto index = size_.fetch_add(len, std::memory_order_relaxed);
1305 auto binfo = bucketAndSubIndex(index);
1306 auto bend = bucketAndSubIndex(index + len);
1307 allocateBufferRange(binfo, len, bend);
1308 internalFillDefaultN(e, len);
1309 return std::move_backward(pos, const_iterator(e), e + len) - len;
1310 }
1311
1312 alignas(kCacheLineSize) cv::ConVecBuffer<
1313 T,
1314 SizeTraits::kDefaultCapacity / 2,
1315 SizeTraits::kMaxVectorSize,
1316 Traits::kPreferBuffersInline,
1317 Traits::kReallocStrategy> buffers_;
1318
1319 static constexpr size_t kMaxBuffers =
1320 detail::log2const(SizeTraits::kMaxVectorSize / (SizeTraits::kDefaultCapacity / 2)) + 1;
1321
1322#if DISPENSO_HAS_CACHED_PTRS
1323 // Non-atomic cache of buffer pointers for read-hot paths. Buffer pointers are write-once
1324 // (never change after allocation) and are stored here BEFORE the release store to buffers_[],
1325 // so any thread that acquires from buffers_[] is guaranteed to see the cached value.
1326 // All concurrent writes to a given slot store the same value.
1327 // Disabled on ARM where cache-line invalidation pressure exceeds the read-path benefit.
1328 alignas(kCacheLineSize) mutable T* cachedPtrs_[kMaxBuffers];
1329#endif
1330
1331 size_t firstBucketShift_;
1332 size_t firstBucketLen_;
1333
1334 // Head of the list of gaps left by growth whose element constructor threw; null until that
1335 // happens. Declared in every build so the layout does not depend on whether exceptions are
1336 // enabled, but only read or written when they are.
1337 std::atomic<Gap*> gaps_{nullptr};
1338
1339 alignas(kCacheLineSize) std::atomic<size_t> size_{0};
1340
1341 DISPENSO_INLINE T* cachedBuffer(size_t bucket) const {
1342#if DISPENSO_HAS_CACHED_PTRS
1343 return cachedPtrs_[bucket];
1344#else
1345 // Relaxed is sufficient: callers only access indices that have been fully constructed, which
1346 // requires an external happens-before with the writing thread. That synchronization
1347 // transitively carries the buffer pointer visibility (which was release-stored during
1348 // allocation). This matches the original pre-cache memory ordering and avoids the cost of
1349 // ldar (load-acquire) on AArch64.
1350 return buffers_[bucket].load(std::memory_order_relaxed);
1351#endif
1352 }
1353
1354 DISPENSO_INLINE void allocateBuffer(const cv::BucketInfo& binfo) {
1355#if DISPENSO_HAS_CACHED_PTRS
1356 buffers_.allocAsNecessary(binfo, cachedPtrs_);
1357#else
1358 buffers_.allocAsNecessary(binfo);
1359#endif
1360 }
1361
1362 DISPENSO_INLINE void
1363 allocateBufferRange(const cv::BucketInfo& binfo, ssize_t rangeLen, const cv::BucketInfo& bend) {
1364#if DISPENSO_HAS_CACHED_PTRS
1365 buffers_.allocAsNecessary(binfo, rangeLen, bend, cachedPtrs_);
1366#else
1367 buffers_.allocAsNecessary(binfo, rangeLen, bend);
1368#endif
1369 }
1370
1371 DISPENSO_INLINE void initCachedPtrs() {
1372#if DISPENSO_HAS_CACHED_PTRS
1373 for (size_t i = 0; i < kMaxBuffers; ++i) {
1374 cachedPtrs_[i] = nullptr;
1375 }
1376#endif
1377 }
1378
1379 DISPENSO_INLINE void setCachedPtr(size_t bucket, T* ptr) {
1380#if DISPENSO_HAS_CACHED_PTRS
1381 cachedPtrs_[bucket] = ptr;
1382#else
1383 (void)bucket;
1384 (void)ptr;
1385#endif
1386 }
1387
1388 DISPENSO_INLINE void clearCachedPtr(size_t bucket) {
1389#if DISPENSO_HAS_CACHED_PTRS
1390 cachedPtrs_[bucket] = nullptr;
1391#else
1392 (void)bucket;
1393#endif
1394 }
1395
1396 DISPENSO_INLINE void moveCachedPtrsFrom(ConcurrentVector& other) {
1397#if DISPENSO_HAS_CACHED_PTRS
1398 for (size_t i = 0; i < kMaxBuffers; ++i) {
1399 cachedPtrs_[i] = other.cachedPtrs_[i];
1400 other.cachedPtrs_[i] = nullptr;
1401 }
1402#else
1403 (void)other;
1404#endif
1405 }
1406
1407 DISPENSO_INLINE void swapCachedPtrs(ConcurrentVector& other) {
1408#if DISPENSO_HAS_CACHED_PTRS
1409 for (size_t i = 0; i < kMaxBuffers; ++i) {
1410 T* cur = cachedPtrs_[i];
1411 cachedPtrs_[i] = other.cachedPtrs_[i];
1412 other.cachedPtrs_[i] = cur;
1413 }
1414#else
1415 (void)other;
1416#endif
1417 }
1418
1419 friend class cv::ConVecIterBase<ConcurrentVector<T, Traits>, T>;
1420 friend class cv::ConcurrentVectorIterator<ConcurrentVector<T, Traits>, T, false>;
1421 friend class cv::ConcurrentVectorIterator<ConcurrentVector<T, Traits>, T, true>;
1422};
1423
1424template <typename T, class Traits1, class Traits2>
1425inline bool operator==(
1426 const ConcurrentVector<T, Traits1>& a,
1427 const ConcurrentVector<T, Traits2>& b) {
1428 return a.size() == b.size() && std::equal(a.begin(), a.end(), b.begin());
1429}
1430
1431template <typename T, class Traits1, class Traits2>
1432inline bool operator!=(
1433 const ConcurrentVector<T, Traits1>& a,
1434 const ConcurrentVector<T, Traits2>& b) {
1435 return !(a == b);
1436}
1437
1438template <typename T, class Traits1, class Traits2>
1439inline bool operator<(
1440 const ConcurrentVector<T, Traits1>& a,
1441 const ConcurrentVector<T, Traits2>& b) {
1442 return std::lexicographical_compare(a.begin(), a.end(), b.begin(), b.end());
1443}
1444
1445template <typename T, class Traits1, class Traits2>
1446inline bool operator>(
1447 const ConcurrentVector<T, Traits1>& a,
1448 const ConcurrentVector<T, Traits2>& b) {
1449 return b < a;
1450}
1451
1452template <typename T, class Traits1, class Traits2>
1453inline bool operator<=(
1454 const ConcurrentVector<T, Traits1>& a,
1455 const ConcurrentVector<T, Traits2>& b) {
1456 return !(b < a);
1457}
1458
1459template <typename T, class Traits1, class Traits2>
1460inline bool operator>=(
1461 const ConcurrentVector<T, Traits1>& a,
1462 const ConcurrentVector<T, Traits2>& b) {
1463 return !(a < b);
1464}
1465
1466template <typename T, class Traits>
1467inline void swap(ConcurrentVector<T, Traits>& a, ConcurrentVector<T, Traits>& b) {
1468 a.swap(b);
1469}
1470
1471// Textual inclusion. Includes undocumented implementation details, e.g. iterators.
1472#include <dispenso/detail/concurrent_vector_impl2.h>
1473
1474} // namespace dispenso
iterator insert(const_iterator pos, const T &value)
iterator grow_by(It start, It end)
const_reverse_iterator rend() const
iterator emplace_back(Args &&... args)
iterator push_back(const T &val)
iterator erase(const_iterator first, const_iterator last)
const_iterator end() const
ConcurrentVector(size_t startSize, const T &defaultValue)
void assign(size_type count, const T &value)
void resize(difference_type len)
void reserve(difference_type capacity)
iterator insert(const_iterator pos, T &&value)
iterator insert(const_iterator pos, size_type count, const T &value)
const_reverse_iterator rbegin() const
iterator grow_to_at_least(size_type n)
ConcurrentVector(size_t startSize)
ConcurrentVector(const ConcurrentVector &other)
iterator grow_to_at_least(size_type n, const T &t)
const_iterator cend() const
constexpr size_type max_size() const noexcept
void resize(difference_type len, const T &value)
const_iterator begin() const
ConcurrentVector(InIterator start, InIterator end)
void assign(It start, It end)
const_iterator cbegin() const
size_type default_capacity() const
iterator insert(const_iterator pos, InputIt first, InputIt last)
iterator grow_by(std::initializer_list< T > initList)
ConcurrentVector(std::initializer_list< T > l)
iterator insert(const_iterator pos, std::initializer_list< T > ilist)
iterator grow_by(size_type delta)
iterator grow_by_generator(size_type delta, Gen gen)
ConcurrentVector(size_type startSize, InIterator start, InIterator end)
ConcurrentVector(size_t startCapacity, ReserveTagS)
iterator erase(const_iterator pos)
const T & at(size_type index) const
ConcurrentVector(ConcurrentVector &&other)
iterator grow_by(size_type delta, const T &t)
constexpr ReserveTagS ReserveTag
constexpr size_t kCacheLineSize
A constant that defines a safe number of bytes+alignment to avoid false sharing.
Definition platform.h:141
static constexpr size_t kMaxVectorSize
The maximum possible size for the vector.
static constexpr bool kPreferBuffersInline
Prefer to place the pointers to the buffers inline in the class.
static constexpr ConcurrentVectorReallocStrategy kReallocStrategy
How far/whether to allocate before memory is required.
static constexpr bool kIteratorPreferSpeed
Should we prefer faster, but larger iterators, or slower, but smaller iterators.
uint32_t log2(uint64_t v)
Compute log base 2 of a value (runtime).
Definition util.h:193
constexpr uint64_t nextPow2(uint64_t v)
Round up to the next power of 2.
Definition util.h:151