Skip to content

Commit 31824b2

Browse files
authored
[libc++] Fix shrink_to_fit to swap buffer only when capacity is strictly smaller (#127321)
The current implementation of the `shrink_to_fit()` function of `basic_string` swaps to the newly allocated buffer when the new buffer has the same capacity as the existing one. While this is not incorrect, it is truly unnecessary to swap to an equally-sized buffer. With equal capacity, we should keep using the existing buffer and simply deallocate the new one, avoiding the extra work of copying elements. The desired behavior was documented in the following comment within the function: https://github.com/llvm/llvm-project/blob/61ad08792a86e62309b982189a600f4342a38d91/libcxx/include/string#L3560-L3566 However, the existing implementation did not exactly conform to this guideline, which is a QoI matter. This PR modifies the `shrink_to_fit()` function to ensure that the buffer is only swapped when the new allocation is strictly smaller than the existing one. When the capacities are equal, the new buffer will be discarded without copying the elements. This is achieved by including the `==` check in the above conditional logic.
1 parent eb92157 commit 31824b2

File tree

4 files changed

+122
-21
lines changed

4 files changed

+122
-21
lines changed

libcxx/include/string

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3495,7 +3495,7 @@ inline _LIBCPP_CONSTEXPR_SINCE_CXX20 void basic_string<_CharT, _Traits, _Allocat
34953495
// The Standard mandates shrink_to_fit() does not increase the capacity.
34963496
// With equal capacity keep the existing buffer. This avoids extra work
34973497
// due to swapping the elements.
3498-
if (__allocation.count - 1 > capacity()) {
3498+
if (__allocation.count - 1 >= capacity()) {
34993499
__alloc_traits::deallocate(__alloc_, __allocation.ptr, __allocation.count);
35003500
return;
35013501
}

libcxx/test/libcxx/strings/basic.string/string.capacity/shrink_to_fit.pass.cpp

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,12 @@
1212

1313
// void shrink_to_fit(); // constexpr since C++20
1414

15-
// Make sure we use an allocation returned by allocate_at_least if it is smaller than the current allocation
16-
// even if it contains more bytes than we requested
17-
1815
#include <cassert>
1916
#include <string>
2017

18+
#include "asan_testing.h"
19+
#include "increasing_allocator.h"
20+
2121
template <typename T>
2222
struct oversizing_allocator {
2323
using value_type = T;
@@ -37,6 +37,9 @@ bool operator==(oversizing_allocator<T>, oversizing_allocator<U>) {
3737
return true;
3838
}
3939

40+
// Make sure we use an allocation returned by allocate_at_least if it is smaller than the current allocation
41+
// even if it contains more bytes than we requested.
42+
// Fix issue: https://github.com/llvm/llvm-project/pull/115659
4043
void test_oversizing_allocator() {
4144
std::basic_string<char, std::char_traits<char>, oversizing_allocator<char>> s{
4245
"String does not fit in the internal buffer and is a bit longer"};
@@ -48,8 +51,37 @@ void test_oversizing_allocator() {
4851
assert(s.size() == size);
4952
}
5053

54+
// Make sure libc++ shrink_to_fit does NOT swap buffer with equal allocation sizes
55+
void test_no_swap_with_equal_allocation_size() {
56+
{ // Test with custom allocator with a minimum allocation size
57+
std::basic_string<char, std::char_traits<char>, min_size_allocator<128, char> > s(
58+
"A long string exceeding SSO limit but within min alloc size");
59+
std::size_t capacity = s.capacity();
60+
std::size_t size = s.size();
61+
auto data = s.data();
62+
s.shrink_to_fit();
63+
assert(s.capacity() <= capacity);
64+
assert(s.size() == size);
65+
assert(is_string_asan_correct(s));
66+
assert(s.capacity() == capacity && s.data() == data);
67+
}
68+
{ // Test with custom allocator with a minimum power of two allocation size
69+
std::basic_string<char, std::char_traits<char>, pow2_allocator<char> > s(
70+
"This is a long string that exceeds the SSO limit");
71+
std::size_t capacity = s.capacity();
72+
std::size_t size = s.size();
73+
auto data = s.data();
74+
s.shrink_to_fit();
75+
assert(s.capacity() <= capacity);
76+
assert(s.size() == size);
77+
assert(is_string_asan_correct(s));
78+
assert(s.capacity() == capacity && s.data() == data);
79+
}
80+
}
81+
5182
int main(int, char**) {
5283
test_oversizing_allocator();
84+
test_no_swap_with_equal_allocation_size();
5385

5486
return 0;
5587
}

libcxx/test/std/strings/basic.string/string.capacity/shrink_to_fit.pass.cpp

Lines changed: 14 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -61,28 +61,25 @@ TEST_CONSTEXPR_CXX20 bool test() {
6161
test_string<std::basic_string<char, std::char_traits<char>, safe_allocator<char>>>();
6262
#endif
6363

64-
return true;
65-
}
66-
6764
#if TEST_STD_VER >= 23
68-
// https://github.com/llvm/llvm-project/issues/95161
69-
void test_increasing_allocator() {
70-
std::basic_string<char, std::char_traits<char>, increasing_allocator<char>> s{
71-
"String does not fit in the internal buffer"};
72-
std::size_t capacity = s.capacity();
73-
std::size_t size = s.size();
74-
s.shrink_to_fit();
75-
assert(s.capacity() <= capacity);
76-
assert(s.size() == size);
77-
LIBCPP_ASSERT(is_string_asan_correct(s));
65+
{ // Make sure shrink_to_fit never increases capacity
66+
// See: https://github.com/llvm/llvm-project/issues/95161
67+
std::basic_string<char, std::char_traits<char>, increasing_allocator<char>> s{
68+
"String does not fit in the internal buffer"};
69+
std::size_t capacity = s.capacity();
70+
std::size_t size = s.size();
71+
s.shrink_to_fit();
72+
assert(s.capacity() <= capacity);
73+
assert(s.size() == size);
74+
LIBCPP_ASSERT(is_string_asan_correct(s));
75+
}
76+
#endif
77+
78+
return true;
7879
}
79-
#endif // TEST_STD_VER >= 23
8080

8181
int main(int, char**) {
8282
test();
83-
#if TEST_STD_VER >= 23
84-
test_increasing_allocator();
85-
#endif
8683
#if TEST_STD_VER > 17
8784
static_assert(test());
8885
#endif

libcxx/test/support/increasing_allocator.h

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
#define TEST_SUPPORT_INCREASING_ALLOCATOR_H
1111

1212
#include <cstddef>
13+
#include <limits>
1314
#include <memory>
1415

1516
#include "test_macros.h"
@@ -49,4 +50,75 @@ TEST_CONSTEXPR_CXX20 bool operator==(increasing_allocator<T>, increasing_allocat
4950
return true;
5051
}
5152

53+
template <std::size_t MinAllocSize, typename T>
54+
class min_size_allocator {
55+
public:
56+
using value_type = T;
57+
min_size_allocator() = default;
58+
59+
template <typename U>
60+
TEST_CONSTEXPR_CXX20 min_size_allocator(const min_size_allocator<MinAllocSize, U>&) TEST_NOEXCEPT {}
61+
62+
TEST_NODISCARD TEST_CONSTEXPR_CXX20 T* allocate(std::size_t n) {
63+
if (n < MinAllocSize)
64+
n = MinAllocSize;
65+
return static_cast<T*>(::operator new(n * sizeof(T)));
66+
}
67+
68+
TEST_CONSTEXPR_CXX20 void deallocate(T* p, std::size_t) TEST_NOEXCEPT { ::operator delete(static_cast<void*>(p)); }
69+
70+
template <typename U>
71+
struct rebind {
72+
using other = min_size_allocator<MinAllocSize, U>;
73+
};
74+
};
75+
76+
template <std::size_t MinAllocSize, typename T, typename U>
77+
TEST_CONSTEXPR_CXX20 bool
78+
operator==(const min_size_allocator<MinAllocSize, T>&, const min_size_allocator<MinAllocSize, U>&) {
79+
return true;
80+
}
81+
82+
template <std::size_t MinAllocSize, typename T, typename U>
83+
TEST_CONSTEXPR_CXX20 bool
84+
operator!=(const min_size_allocator<MinAllocSize, T>&, const min_size_allocator<MinAllocSize, U>&) {
85+
return false;
86+
}
87+
88+
template <typename T>
89+
class pow2_allocator {
90+
public:
91+
using value_type = T;
92+
pow2_allocator() = default;
93+
94+
template <typename U>
95+
TEST_CONSTEXPR_CXX20 pow2_allocator(const pow2_allocator<U>&) TEST_NOEXCEPT {}
96+
97+
TEST_NODISCARD TEST_CONSTEXPR_CXX20 T* allocate(std::size_t n) {
98+
return static_cast<T*>(::operator new(next_power_of_two(n) * sizeof(T)));
99+
}
100+
101+
TEST_CONSTEXPR_CXX20 void deallocate(T* p, std::size_t) TEST_NOEXCEPT { ::operator delete(static_cast<void*>(p)); }
102+
103+
private:
104+
TEST_CONSTEXPR_CXX20 std::size_t next_power_of_two(std::size_t n) const {
105+
if ((n & (n - 1)) == 0)
106+
return n;
107+
for (std::size_t shift = 1; shift < std::numeric_limits<std::size_t>::digits; shift <<= 1) {
108+
n |= n >> shift;
109+
}
110+
return n + 1;
111+
}
112+
};
113+
114+
template <typename T, typename U>
115+
TEST_CONSTEXPR_CXX20 bool operator==(const pow2_allocator<T>&, const pow2_allocator<U>&) {
116+
return true;
117+
}
118+
119+
template <typename T, typename U>
120+
TEST_CONSTEXPR_CXX20 bool operator!=(const pow2_allocator<T>&, const pow2_allocator<U>&) {
121+
return false;
122+
}
123+
52124
#endif // TEST_SUPPORT_INCREASING_ALLOCATOR_H

0 commit comments

Comments
 (0)