Skip to content

Commit ac58253

Browse files
committed
[libc++] Take advantage of trivial relocation in std::vector::erase
In vector::erase(iter) and vector::erase(iter, iter), we can take advantage of a type being trivially relocatable to open up a gap in the vector and then relocate the tail of the vector into that gap. The benefit is that relocating an object is often more efficient than move-assigning and then destroying the original object. For types that can be relocated trivially but that are complicated enough for the compiler not to optimize by itself (like std::string), this provides around a 2x performance speedup in vector::erase (see below). This optimization requires stopping the usage of Clang's __is_trivially_relocatable builtin, which doesn't currently honour assignment operators like is_trivially_copyable does and can lead us to perform incorrect optimizations. It is also worth noting that __uninitialized_allocator_relocate has to be modified so that we can relocate into an overlapping range. This has an unfortunate impact on its exception safety guarantees, which needs to be investigated further. Previous implementation -------------------------------------------------------------------------------------- Benchmark Time CPU Iterations -------------------------------------------------------------------------------------- BM_erase_iter_in_middle/vector_int/1024 24.9 ns 24.9 ns 28042962 BM_erase_iter_in_middle/vector_int/4096 107 ns 107 ns 6590592 BM_erase_iter_in_middle/vector_int/10240 271 ns 265 ns 2733478 BM_erase_iter_in_middle/vector_string/1024 349 ns 349 ns 2005886 BM_erase_iter_in_middle/vector_string/4096 1410 ns 1406 ns 498355 BM_erase_iter_in_middle/vector_string/10240 3449 ns 3449 ns 201989 BM_erase_iter_at_start/vector_int/1024 47.1 ns 47.1 ns 14836261 BM_erase_iter_at_start/vector_int/4096 204 ns 204 ns 3430414 BM_erase_iter_at_start/vector_int/10240 504 ns 504 ns 1391373 BM_erase_iter_at_start/vector_string/1024 684 ns 684 ns 1025160 BM_erase_iter_at_start/vector_string/4096 2855 ns 2806 ns 254080 BM_erase_iter_at_start/vector_string/10240 7060 ns 7060 ns 94134 New implementation -------------------------------------------------------------------------------------- Benchmark Time CPU Iterations -------------------------------------------------------------------------------------- BM_erase_iter_in_middle/vector_int/1024 26.0 ns 25.9 ns 27127367 BM_erase_iter_in_middle/vector_int/4096 105 ns 105 ns 6515204 BM_erase_iter_in_middle/vector_int/10240 259 ns 258 ns 2800795 BM_erase_iter_in_middle/vector_string/1024 148 ns 147 ns 4725706 BM_erase_iter_in_middle/vector_string/4096 608 ns 606 ns 1168205 BM_erase_iter_in_middle/vector_string/10240 1523 ns 1520 ns 459909 BM_erase_iter_at_start/vector_int/1024 47.1 ns 47.1 ns 14762513 BM_erase_iter_at_start/vector_int/4096 205 ns 205 ns 3403130 BM_erase_iter_at_start/vector_int/10240 507 ns 507 ns 1382716 BM_erase_iter_at_start/vector_string/1024 300 ns 300 ns 2327546 BM_erase_iter_at_start/vector_string/4096 1205 ns 1205 ns 580855 BM_erase_iter_at_start/vector_string/10240 4296 ns 4296 ns 162956
1 parent 9790cf1 commit ac58253

File tree

5 files changed

+35
-98
lines changed

5 files changed

+35
-98
lines changed

libcxx/docs/ReleaseNotes/20.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@ Improvements and New Features
5252
- The ``lexicographical_compare`` and ``ranges::lexicographical_compare`` algorithms have been optimized for trivially
5353
equality comparable types, resulting in a performance improvement of up to 40x.
5454

55+
- The ``std::vector::erase`` function has been optimized for types that can be relocated trivially (such as ``std::string``),
56+
yielding speed ups witnessed to be around 2x for these types (but subject to the use case).
57+
5558
- The ``_LIBCPP_ENABLE_CXX20_REMOVED_TEMPORARY_BUFFER`` macro has been added to make ``std::get_temporary_buffer`` and
5659
``std::return_temporary_buffer`` available.
5760

libcxx/include/__vector/vector.h

Lines changed: 32 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,13 @@
3434
#include <__memory/allocator.h>
3535
#include <__memory/allocator_traits.h>
3636
#include <__memory/compressed_pair.h>
37+
#include <__memory/is_trivially_allocator_relocatable.h>
3738
#include <__memory/noexcept_move_assign_container.h>
3839
#include <__memory/pointer_traits.h>
3940
#include <__memory/swap_allocator.h>
4041
#include <__memory/temp_value.h>
4142
#include <__memory/uninitialized_algorithms.h>
43+
#include <__memory/uninitialized_relocate.h>
4244
#include <__ranges/access.h>
4345
#include <__ranges/concepts.h>
4446
#include <__ranges/container_compatible_range.h>
@@ -515,8 +517,36 @@ class _LIBCPP_TEMPLATE_VIS vector {
515517
}
516518
#endif
517519

518-
_LIBCPP_CONSTEXPR_SINCE_CXX20 _LIBCPP_HIDE_FROM_ABI iterator erase(const_iterator __position);
519-
_LIBCPP_CONSTEXPR_SINCE_CXX20 _LIBCPP_HIDE_FROM_ABI iterator erase(const_iterator __first, const_iterator __last);
520+
_LIBCPP_CONSTEXPR_SINCE_CXX20 _LIBCPP_HIDE_FROM_ABI iterator erase(const_iterator __position) {
521+
_LIBCPP_ASSERT_VALID_ELEMENT_ACCESS(
522+
__position != end(), "vector::erase(iterator) called with a non-dereferenceable iterator");
523+
return erase(__position, __position + 1);
524+
}
525+
_LIBCPP_CONSTEXPR_SINCE_CXX20 _LIBCPP_HIDE_FROM_ABI iterator erase(const_iterator __cfirst, const_iterator __clast) {
526+
_LIBCPP_ASSERT_VALID_INPUT_RANGE(__cfirst <= __clast, "vector::erase(first, last) called with invalid range");
527+
528+
iterator __first = begin() + std::distance(cbegin(), __cfirst);
529+
iterator __last = begin() + std::distance(cbegin(), __clast);
530+
if (__first == __last)
531+
return __last;
532+
533+
auto __n = std::distance(__first, __last);
534+
535+
// If the value_type is nothrow move constructible, we destroy the range being erased and we
536+
// relocate the tail of the vector into the created gap. This is especially efficient if the
537+
// elements are trivially relocatable. Otherwise, we use the standard technique with move-assignments.
538+
if constexpr (is_nothrow_move_constructible<value_type>::value) {
539+
std::__allocator_destroy(this->__alloc_, __first, __last);
540+
std::__uninitialized_allocator_relocate(this->__alloc_, __last, end(), __first);
541+
} else {
542+
auto __new_end = std::move(__last, end(), __first);
543+
std::__allocator_destroy(this->__alloc_, __new_end, end());
544+
}
545+
546+
this->__end_ -= __n;
547+
__annotate_shrink(size() + __n);
548+
return __first;
549+
}
520550

521551
_LIBCPP_CONSTEXPR_SINCE_CXX20 _LIBCPP_HIDE_FROM_ABI void clear() _NOEXCEPT {
522552
size_type __old_size = size();
@@ -1108,28 +1138,6 @@ _LIBCPP_CONSTEXPR_SINCE_CXX20 inline
11081138
#endif
11091139
}
11101140

1111-
template <class _Tp, class _Allocator>
1112-
_LIBCPP_CONSTEXPR_SINCE_CXX20 inline _LIBCPP_HIDE_FROM_ABI typename vector<_Tp, _Allocator>::iterator
1113-
vector<_Tp, _Allocator>::erase(const_iterator __position) {
1114-
_LIBCPP_ASSERT_VALID_ELEMENT_ACCESS(
1115-
__position != end(), "vector::erase(iterator) called with a non-dereferenceable iterator");
1116-
difference_type __ps = __position - cbegin();
1117-
pointer __p = this->__begin_ + __ps;
1118-
this->__destruct_at_end(std::move(__p + 1, this->__end_, __p));
1119-
return __make_iter(__p);
1120-
}
1121-
1122-
template <class _Tp, class _Allocator>
1123-
_LIBCPP_CONSTEXPR_SINCE_CXX20 typename vector<_Tp, _Allocator>::iterator
1124-
vector<_Tp, _Allocator>::erase(const_iterator __first, const_iterator __last) {
1125-
_LIBCPP_ASSERT_VALID_INPUT_RANGE(__first <= __last, "vector::erase(first, last) called with invalid range");
1126-
pointer __p = this->__begin_ + (__first - begin());
1127-
if (__first != __last) {
1128-
this->__destruct_at_end(std::move(__p + (__last - __first), this->__end_, __p));
1129-
}
1130-
return __make_iter(__p);
1131-
}
1132-
11331141
template <class _Tp, class _Allocator>
11341142
_LIBCPP_CONSTEXPR_SINCE_CXX20 void
11351143
vector<_Tp, _Allocator>::__move_range(pointer __from_s, pointer __from_e, pointer __to) {

libcxx/test/std/containers/sequences/vector/vector.modifiers/common.h

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -40,28 +40,6 @@ struct Throws {
4040
bool Throws::sThrows = false;
4141
#endif
4242

43-
struct Tracker {
44-
int copy_assignments = 0;
45-
int move_assignments = 0;
46-
};
47-
48-
struct TrackedAssignment {
49-
Tracker* tracker_;
50-
TEST_CONSTEXPR_CXX14 explicit TrackedAssignment(Tracker* tracker) : tracker_(tracker) {}
51-
52-
TrackedAssignment(TrackedAssignment const&) = default;
53-
TrackedAssignment(TrackedAssignment&&) = default;
54-
55-
TEST_CONSTEXPR_CXX14 TrackedAssignment& operator=(TrackedAssignment const&) {
56-
tracker_->copy_assignments++;
57-
return *this;
58-
}
59-
TEST_CONSTEXPR_CXX14 TrackedAssignment& operator=(TrackedAssignment&&) {
60-
tracker_->move_assignments++;
61-
return *this;
62-
}
63-
};
64-
6543
struct NonTriviallyRelocatable {
6644
int value_;
6745
TEST_CONSTEXPR NonTriviallyRelocatable() : value_(0) {}

libcxx/test/std/containers/sequences/vector/vector.modifiers/erase_iter.pass.cpp

Lines changed: 0 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -107,31 +107,5 @@ int main(int, char**) {
107107
}
108108
#endif
109109

110-
// Make sure we satisfy the complexity requirement in terms of the number of times the assignment
111-
// operator is called.
112-
//
113-
// There is currently ambiguity as to whether this is truly mandated by the Standard, so we only
114-
// test it for libc++.
115-
#ifdef _LIBCPP_VERSION
116-
{
117-
Tracker tracker;
118-
std::vector<TrackedAssignment> v;
119-
120-
// Set up the vector with 5 elements.
121-
for (int i = 0; i != 5; ++i) {
122-
v.emplace_back(&tracker);
123-
}
124-
assert(tracker.copy_assignments == 0);
125-
assert(tracker.move_assignments == 0);
126-
127-
// Erase element [1] from it. Elements [2] [3] [4] should be shifted, so we should
128-
// see 3 move assignments (and nothing else).
129-
v.erase(v.begin() + 1);
130-
assert(v.size() == 4);
131-
assert(tracker.copy_assignments == 0);
132-
assert(tracker.move_assignments == 3);
133-
}
134-
#endif
135-
136110
return 0;
137111
}

libcxx/test/std/containers/sequences/vector/vector.modifiers/erase_iter_iter.pass.cpp

Lines changed: 0 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -196,31 +196,5 @@ int main(int, char**) {
196196
assert(it == v.begin() + 2);
197197
}
198198

199-
// Make sure we satisfy the complexity requirement in terms of the number of times the assignment
200-
// operator is called.
201-
//
202-
// There is currently ambiguity as to whether this is truly mandated by the Standard, so we only
203-
// test it for libc++.
204-
#ifdef _LIBCPP_VERSION
205-
{
206-
Tracker tracker;
207-
std::vector<TrackedAssignment> v;
208-
209-
// Set up the vector with 5 elements.
210-
for (int i = 0; i != 5; ++i) {
211-
v.emplace_back(&tracker);
212-
}
213-
assert(tracker.copy_assignments == 0);
214-
assert(tracker.move_assignments == 0);
215-
216-
// Erase elements [1] and [2] from it. Elements [3] [4] should be shifted, so we should
217-
// see 2 move assignments (and nothing else).
218-
v.erase(v.begin() + 1, v.begin() + 3);
219-
assert(v.size() == 3);
220-
assert(tracker.copy_assignments == 0);
221-
assert(tracker.move_assignments == 2);
222-
}
223-
#endif
224-
225199
return 0;
226200
}

0 commit comments

Comments
 (0)