Skip to content

Commit f5e9693

Browse files
committed
[libc++] Optimize vector growing of trivially relocatable types
1 parent 40a631f commit f5e9693

File tree

11 files changed

+285
-57
lines changed

11 files changed

+285
-57
lines changed

libcxx/benchmarks/ContainerBenchmarks.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ void BM_ConstructFromRange(benchmark::State& st, Container, GenInputs gen) {
8080
}
8181

8282
template <class Container>
83-
void BM_Pushback(benchmark::State& state, Container c) {
83+
void BM_Pushback_no_grow(benchmark::State& state, Container c) {
8484
int count = state.range(0);
8585
c.reserve(count);
8686
while (state.KeepRunningBatch(count)) {

libcxx/benchmarks/vector_operations.bench.cpp

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#include <cstdint>
22
#include <cstdlib>
33
#include <cstring>
4+
#include <deque>
45
#include <functional>
56
#include <vector>
67

@@ -39,6 +40,21 @@ BENCHMARK_CAPTURE(BM_ConstructFromRange, vector_size_t, std::vector<size_t>{}, g
3940
BENCHMARK_CAPTURE(BM_ConstructFromRange, vector_string, std::vector<std::string>{}, getRandomStringInputs)
4041
->Arg(TestNumInputs);
4142

42-
BENCHMARK_CAPTURE(BM_Pushback, vector_int, std::vector<int>{})->Arg(TestNumInputs);
43+
BENCHMARK_CAPTURE(BM_Pushback_no_grow, vector_int, std::vector<int>{})->Arg(TestNumInputs);
44+
45+
template <class T>
46+
void bm_grow(benchmark::State& state) {
47+
for (auto _ : state) {
48+
std::vector<T> vec;
49+
benchmark::DoNotOptimize(vec);
50+
for (size_t i = 0; i != 2048; ++i)
51+
vec.emplace_back();
52+
benchmark::DoNotOptimize(vec);
53+
}
54+
}
55+
BENCHMARK(bm_grow<int>);
56+
BENCHMARK(bm_grow<std::string>);
57+
BENCHMARK(bm_grow<std::unique_ptr<int>>);
58+
BENCHMARK(bm_grow<std::deque<int>>);
4359

4460
BENCHMARK_MAIN();

libcxx/docs/ReleaseNotes/18.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,8 @@ Improvements and New Features
7171
- ``std::for_each`` has been optimized for segmented iterators like ``std::deque::iterator`` in C++23 and
7272
later, which can lead up to 40x performance improvements.
7373

74+
- The performance of growing ``std::vector`` has been improved for trivially relocatable types.
75+
7476
- The library now provides several hardening modes under which common cases of library undefined behavior will be turned
7577
into a reliable program termination. The ``fast`` hardening mode enables a set of security-critical checks with
7678
minimal runtime overhead; the ``extensive`` hardening mode additionally enables relatively cheap checks that catch

libcxx/include/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -803,6 +803,7 @@ set(files
803803
__type_traits/is_trivially_lexicographically_comparable.h
804804
__type_traits/is_trivially_move_assignable.h
805805
__type_traits/is_trivially_move_constructible.h
806+
__type_traits/is_trivially_relocatable.h
806807
__type_traits/is_unbounded_array.h
807808
__type_traits/is_union.h
808809
__type_traits/is_unsigned.h

libcxx/include/__memory/uninitialized_algorithms.h

Lines changed: 41 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
#include <__type_traits/is_trivially_copy_constructible.h>
3030
#include <__type_traits/is_trivially_move_assignable.h>
3131
#include <__type_traits/is_trivially_move_constructible.h>
32+
#include <__type_traits/is_trivially_relocatable.h>
3233
#include <__type_traits/is_unbounded_array.h>
3334
#include <__type_traits/negation.h>
3435
#include <__type_traits/remove_const.h>
@@ -591,60 +592,56 @@ __uninitialized_allocator_copy(_Alloc& __alloc, _Iter1 __first1, _Sent1 __last1,
591592
return std::__rewrap_iter(__first2, __result);
592593
}
593594

594-
// Move-construct the elements [__first1, __last1) into [__first2, __first2 + N)
595-
// if the move constructor is noexcept, where N is distance(__first1, __last1).
596-
//
597-
// Otherwise try to copy all elements. If an exception is thrown the already copied
598-
// elements are destroyed in reverse order of their construction.
599-
template <class _Alloc, class _Iter1, class _Sent1, class _Iter2>
600-
_LIBCPP_HIDE_FROM_ABI _LIBCPP_CONSTEXPR_SINCE_CXX20 _Iter2
601-
__uninitialized_allocator_move_if_noexcept(_Alloc& __alloc, _Iter1 __first1, _Sent1 __last1, _Iter2 __first2) {
602-
static_assert(__is_cpp17_move_insertable<_Alloc>::value,
603-
"The specified type does not meet the requirements of Cpp17MoveInsertable");
604-
auto __destruct_first = __first2;
605-
auto __guard =
606-
std::__make_exception_guard(_AllocatorDestroyRangeReverse<_Alloc, _Iter2>(__alloc, __destruct_first, __first2));
607-
while (__first1 != __last1) {
608-
#ifndef _LIBCPP_HAS_NO_EXCEPTIONS
609-
allocator_traits<_Alloc>::construct(__alloc, std::__to_address(__first2), std::move_if_noexcept(*__first1));
610-
#else
611-
allocator_traits<_Alloc>::construct(__alloc, std::__to_address(__first2), std::move(*__first1));
612-
#endif
613-
++__first1;
614-
++__first2;
615-
}
616-
__guard.__complete();
617-
return __first2;
618-
}
619-
620595
template <class _Alloc, class _Type>
621596
struct __allocator_has_trivial_move_construct : _Not<__has_construct<_Alloc, _Type*, _Type&&> > {};
622597

623598
template <class _Type>
624599
struct __allocator_has_trivial_move_construct<allocator<_Type>, _Type> : true_type {};
625600

626-
#ifndef _LIBCPP_COMPILER_GCC
627-
template <
628-
class _Alloc,
629-
class _Iter1,
630-
class _Iter2,
631-
class _Type = typename iterator_traits<_Iter1>::value_type,
632-
class = __enable_if_t<is_trivially_move_constructible<_Type>::value && is_trivially_move_assignable<_Type>::value &&
633-
__allocator_has_trivial_move_construct<_Alloc, _Type>::value> >
634-
_LIBCPP_HIDE_FROM_ABI _LIBCPP_CONSTEXPR_SINCE_CXX20 _Iter2
635-
__uninitialized_allocator_move_if_noexcept(_Alloc&, _Iter1 __first1, _Iter1 __last1, _Iter2 __first2) {
636-
if (__libcpp_is_constant_evaluated()) {
637-
while (__first1 != __last1) {
638-
std::__construct_at(std::__to_address(__first2), std::move(*__first1));
639-
++__first1;
640-
++__first2;
601+
template <class _Alloc, class _Tp>
602+
struct __allocator_has_trivial_destroy : _Not<__has_destroy<_Alloc, _Tp*> > {};
603+
604+
template <class _Tp, class _Up>
605+
struct __allocator_has_trivial_destroy<allocator<_Tp>, _Up> : true_type {};
606+
607+
// __uninitialized_allocator_relocate relocates the objects in [__first, __last) into __result.
608+
// Relocation means that the objects in [__first, __last) are placed into __result as-if by move-construct and destroy,
609+
// except that the move constructor and destructor may never be called if they are known to be equivalent to a memcpy.
610+
//
611+
// Preconditions: __result doesn't contain any objects and [__first, __last) contains objects
612+
// Postconditions: __result contains the objects from [__first, __last) and
613+
// [__first, __last) doesn't contain any objects
614+
//
615+
// The strong exception guarantee is provided if any of the following are true:
616+
// - is_nothrow_move_constructible<_Tp>
617+
// - is_copy_constructible<_Tp>
618+
// - __libcpp_is_trivially_relocatable<_Tp>
619+
template <class _Alloc, class _Tp>
620+
_LIBCPP_HIDE_FROM_ABI _LIBCPP_CONSTEXPR_SINCE_CXX14 void
621+
__uninitialized_allocator_relocate(_Alloc& __alloc, _Tp* __first, _Tp* __last, _Tp* __result) {
622+
static_assert(__is_cpp17_move_insertable<_Alloc>::value,
623+
"The specified type does not meet the requirements of Cpp17MoveInsertable");
624+
if (__libcpp_is_constant_evaluated() || !__libcpp_is_trivially_relocatable<_Tp>::value ||
625+
!__allocator_has_trivial_move_construct<_Alloc, _Tp>::value ||
626+
!__allocator_has_trivial_destroy<_Alloc, _Tp>::value) {
627+
auto __destruct_first = __result;
628+
auto __guard =
629+
std::__make_exception_guard(_AllocatorDestroyRangeReverse<_Alloc, _Tp*>(__alloc, __destruct_first, __result));
630+
while (__first != __last) {
631+
#ifndef _LIBCPP_HAS_NO_EXCEPTIONS
632+
allocator_traits<_Alloc>::construct(__alloc, __result, std::move_if_noexcept(*__first));
633+
#else
634+
allocator_traits<_Alloc>::construct(__alloc, __result, std::move(*__first));
635+
#endif
636+
++__first;
637+
++__result;
641638
}
642-
return __first2;
639+
__guard.__complete();
640+
std::__allocator_destroy(__alloc, __first, __last);
643641
} else {
644-
return std::move(__first1, __last1, __first2);
642+
__builtin_memcpy(__result, __first, sizeof(_Tp) * (__last - __first));
645643
}
646644
}
647-
#endif // _LIBCPP_COMPILER_GCC
648645

649646
_LIBCPP_END_NAMESPACE_STD
650647

libcxx/include/__memory/unique_ptr.h

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
#include <__memory/compressed_pair.h>
2222
#include <__type_traits/add_lvalue_reference.h>
2323
#include <__type_traits/common_type.h>
24+
#include <__type_traits/conditional.h>
2425
#include <__type_traits/dependent_type.h>
2526
#include <__type_traits/integral_constant.h>
2627
#include <__type_traits/is_array.h>
@@ -33,6 +34,7 @@
3334
#include <__type_traits/is_reference.h>
3435
#include <__type_traits/is_same.h>
3536
#include <__type_traits/is_swappable.h>
37+
#include <__type_traits/is_trivially_relocatable.h>
3638
#include <__type_traits/is_void.h>
3739
#include <__type_traits/remove_extent.h>
3840
#include <__type_traits/type_identity.h>
@@ -129,6 +131,17 @@ class _LIBCPP_UNIQUE_PTR_TRIVIAL_ABI _LIBCPP_TEMPLATE_VIS unique_ptr {
129131

130132
static_assert(!is_rvalue_reference<deleter_type>::value, "the specified deleter type cannot be an rvalue reference");
131133

134+
// A unique_ptr contains the following members which may be trivially relocatable:
135+
// - pointer : this may be trivially relocatable, so it's checked
136+
// - deleter_type: this may be trivially relocatable, so it's checked
137+
//
138+
// This unique_ptr implementation only contains a pointer to the unique object and a deleter, so there are no
139+
// references to itself. This means that the entire structure is trivially relocatable if its members are.
140+
using __trivially_relocatable = __conditional_t<
141+
__libcpp_is_trivially_relocatable<pointer>::value && __libcpp_is_trivially_relocatable<deleter_type>::value,
142+
unique_ptr,
143+
void>;
144+
132145
private:
133146
__compressed_pair<pointer, deleter_type> __ptr_;
134147

@@ -276,6 +289,17 @@ class _LIBCPP_UNIQUE_PTR_TRIVIAL_ABI _LIBCPP_TEMPLATE_VIS unique_ptr<_Tp[], _Dp>
276289
typedef _Dp deleter_type;
277290
typedef typename __pointer<_Tp, deleter_type>::type pointer;
278291

292+
// A unique_ptr contains the following members which may be trivially relocatable:
293+
// - pointer : this may be trivially relocatable, so it's checked
294+
// - deleter_type: this may be trivially relocatable, so it's checked
295+
//
296+
// This unique_ptr implementation only contains a pointer to the unique object and a deleter, so there are no
297+
// references to itself. This means that the entire structure is trivially relocatable if its members are.
298+
using __trivially_relocatable = __conditional_t<
299+
__libcpp_is_trivially_relocatable<pointer>::value && __libcpp_is_trivially_relocatable<deleter_type>::value,
300+
unique_ptr,
301+
void>;
302+
279303
private:
280304
__compressed_pair<pointer, deleter_type> __ptr_;
281305

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4+
// See https://llvm.org/LICENSE.txt for license information.
5+
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6+
//
7+
//===----------------------------------------------------------------------===//
8+
9+
#ifndef _LIBCPP___TYPE_TRAITS_IS_TRIVIALLY_RELOCATABLE_H
10+
#define _LIBCPP___TYPE_TRAITS_IS_TRIVIALLY_RELOCATABLE_H
11+
12+
#include <__config>
13+
#include <__type_traits/enable_if.h>
14+
#include <__type_traits/integral_constant.h>
15+
#include <__type_traits/is_same.h>
16+
#include <__type_traits/is_trivially_copyable.h>
17+
18+
#if !defined(_LIBCPP_HAS_NO_PRAGMA_SYSTEM_HEADER)
19+
# pragma GCC system_header
20+
#endif
21+
22+
_LIBCPP_BEGIN_NAMESPACE_STD
23+
24+
// A type is trivially relocatable if a move construct + destroy of the original object is equivalent to
25+
// `memcpy(dst, src, sizeof(T))`.
26+
27+
#if __has_builtin(__is_trivially_relocatable)
28+
template <class _Tp, class = void>
29+
struct __libcpp_is_trivially_relocatable : integral_constant<bool, __is_trivially_relocatable(_Tp)> {};
30+
#else
31+
template <class _Tp, class = void>
32+
struct __libcpp_is_trivially_relocatable : is_trivially_copyable<_Tp> {};
33+
#endif
34+
35+
template <class _Tp>
36+
struct __libcpp_is_trivially_relocatable<_Tp,
37+
__enable_if_t<is_same<_Tp, typename _Tp::__trivially_relocatable>::value> >
38+
: true_type {};
39+
40+
_LIBCPP_END_NAMESPACE_STD
41+
42+
#endif // _LIBCPP___TYPE_TRAITS_IS_TRIVIALLY_RELOCATABLE_H

libcxx/include/module.modulemap.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1970,6 +1970,7 @@ module std_private_type_traits_is_trivially_destructible [system
19701970
module std_private_type_traits_is_trivially_lexicographically_comparable [system] { header "__type_traits/is_trivially_lexicographically_comparable.h" }
19711971
module std_private_type_traits_is_trivially_move_assignable [system] { header "__type_traits/is_trivially_move_assignable.h" }
19721972
module std_private_type_traits_is_trivially_move_constructible [system] { header "__type_traits/is_trivially_move_constructible.h" }
1973+
module std_private_type_traits_is_trivially_relocatable [system] { header "__type_traits/is_trivially_relocatable.h" }
19731974
module std_private_type_traits_is_unbounded_array [system] { header "__type_traits/is_unbounded_array.h" }
19741975
module std_private_type_traits_is_union [system] { header "__type_traits/is_union.h" }
19751976
module std_private_type_traits_is_unsigned [system] { header "__type_traits/is_unsigned.h" }

libcxx/include/string

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -599,6 +599,7 @@ basic_string<char32_t> operator""s( const char32_t *str, size_t len );
599599
#include <__ranges/size.h>
600600
#include <__string/char_traits.h>
601601
#include <__string/extern_template_lists.h>
602+
#include <__type_traits/conditional.h>
602603
#include <__type_traits/is_allocator.h>
603604
#include <__type_traits/is_array.h>
604605
#include <__type_traits/is_convertible.h>
@@ -607,6 +608,7 @@ basic_string<char32_t> operator""s( const char32_t *str, size_t len );
607608
#include <__type_traits/is_same.h>
608609
#include <__type_traits/is_standard_layout.h>
609610
#include <__type_traits/is_trivial.h>
611+
#include <__type_traits/is_trivially_relocatable.h>
610612
#include <__type_traits/noexcept_move_assign_container.h>
611613
#include <__type_traits/remove_cvref.h>
612614
#include <__type_traits/void_t.h>
@@ -724,6 +726,20 @@ public:
724726
typedef typename __alloc_traits::pointer pointer;
725727
typedef typename __alloc_traits::const_pointer const_pointer;
726728

729+
// A basic_string contains the following members which may be trivially relocatable:
730+
// - pointer: is currently assumed to be trivially relocatable, but is still checked in case that changes
731+
// - size_type: is always trivially relocatable, since it has to be an integral type
732+
// - value_type: is always trivially relocatable, since it has to be trivial
733+
// - unsigned char: is a fundamental type, so it's trivially relocatable
734+
// - allocator_type: may or may not be trivially relocatable, so it's checked
735+
//
736+
// This string implementation doesn't contain any references into itself. It only contains a bit that says whether
737+
// it is in small or large string mode, so the entire structure is trivially relocatable if its members are.
738+
using __trivially_relocatable = __conditional_t<
739+
__libcpp_is_trivially_relocatable<allocator_type>::value && __libcpp_is_trivially_relocatable<pointer>::value,
740+
basic_string,
741+
void>;
742+
727743
static_assert((!is_array<value_type>::value), "Character type of basic_string must not be an array");
728744
static_assert((is_standard_layout<value_type>::value), "Character type of basic_string must be standard-layout");
729745
static_assert((is_trivial<value_type>::value), "Character type of basic_string must be trivial");

libcxx/include/vector

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -982,37 +982,54 @@ template <ranges::input_range _Range,
982982
vector(from_range_t, _Range&&, _Alloc = _Alloc()) -> vector<ranges::range_value_t<_Range>, _Alloc>;
983983
#endif
984984

985+
// __swap_out_circular_buffer relocates the objects in [__begin_, __end_) into the front of __v and swaps the buffers of
986+
// *this and __v. It is assumed that __v provides space for exactly (__end_ - __begin_) objects in the front. This
987+
// function has a strong exception guarantee.
985988
template <class _Tp, class _Allocator>
986989
_LIBCPP_CONSTEXPR_SINCE_CXX20 void
987990
vector<_Tp, _Allocator>::__swap_out_circular_buffer(__split_buffer<value_type, allocator_type&>& __v) {
988991
__annotate_delete();
989-
using _RevIter = std::reverse_iterator<pointer>;
990-
__v.__begin_ = std::__uninitialized_allocator_move_if_noexcept(
991-
__alloc(), _RevIter(__end_), _RevIter(__begin_), _RevIter(__v.__begin_))
992-
.base();
992+
auto __new_begin = __v.__begin_ - (__end_ - __begin_);
993+
std::__uninitialized_allocator_relocate(
994+
__alloc(), std::__to_address(__begin_), std::__to_address(__end_), std::__to_address(__new_begin));
995+
__v.__begin_ = __new_begin;
996+
__end_ = __begin_; // All the objects have been destroyed by relocating them.
993997
std::swap(this->__begin_, __v.__begin_);
994998
std::swap(this->__end_, __v.__end_);
995999
std::swap(this->__end_cap(), __v.__end_cap());
9961000
__v.__first_ = __v.__begin_;
9971001
__annotate_new(size());
9981002
}
9991003

1004+
// __swap_out_circular_buffer relocates the objects in [__begin_, __p) into the front of __v, the objects in
1005+
// [__p, __end_) into the back of __v and swaps the buffers of *this and __v. It is assumed that __v provides space for
1006+
// exactly (__p - __begin_) objects in the front and space for at least (__end_ - __p) objects in the back. This
1007+
// function has a strong exception guarantee if __begin_ == __p || __end_ == __p.
10001008
template <class _Tp, class _Allocator>
10011009
_LIBCPP_CONSTEXPR_SINCE_CXX20 typename vector<_Tp, _Allocator>::pointer
10021010
vector<_Tp, _Allocator>::__swap_out_circular_buffer(__split_buffer<value_type, allocator_type&>& __v, pointer __p) {
10031011
__annotate_delete();
1004-
pointer __r = __v.__begin_;
1005-
using _RevIter = std::reverse_iterator<pointer>;
1006-
__v.__begin_ = std::__uninitialized_allocator_move_if_noexcept(
1007-
__alloc(), _RevIter(__p), _RevIter(__begin_), _RevIter(__v.__begin_))
1008-
.base();
1009-
__v.__end_ = std::__uninitialized_allocator_move_if_noexcept(__alloc(), __p, __end_, __v.__end_);
1012+
pointer __ret = __v.__begin_;
1013+
1014+
// Relocate [__p, __end_) first to avoid having a hole in [__begin_, __end_)
1015+
// in case something in [__begin_, __p) throws.
1016+
std::__uninitialized_allocator_relocate(
1017+
__alloc(), std::__to_address(__p), std::__to_address(__end_), std::__to_address(__v.__end_));
1018+
__v.__end_ += (__end_ - __p);
1019+
__end_ = __p; // The objects in [__p, __end_) have been destroyed by relocating them.
1020+
auto __new_begin = __v.__begin_ - (__p - __begin_);
1021+
1022+
std::__uninitialized_allocator_relocate(
1023+
__alloc(), std::__to_address(__begin_), std::__to_address(__p), std::__to_address(__new_begin));
1024+
__v.__begin_ = __new_begin;
1025+
__end_ = __begin_; // All the objects have been destroyed by relocating them.
1026+
10101027
std::swap(this->__begin_, __v.__begin_);
10111028
std::swap(this->__end_, __v.__end_);
10121029
std::swap(this->__end_cap(), __v.__end_cap());
10131030
__v.__first_ = __v.__begin_;
10141031
__annotate_new(size());
1015-
return __r;
1032+
return __ret;
10161033
}
10171034

10181035
template <class _Tp, class _Allocator>

0 commit comments

Comments
 (0)