-
Notifications
You must be signed in to change notification settings - Fork 12.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[libc++][test] Refactor increasing_allocator #115671
Conversation
@llvm/pr-subscribers-libcxx Author: Peng Liu (winner245) ChangesThe > a.allocate(n) This PR addresses the issue by modifying Full diff: https://github.com/llvm/llvm-project/pull/115671.diff 3 Files Affected:
diff --git a/libcxx/test/std/containers/sequences/vector.bool/shrink_to_fit.pass.cpp b/libcxx/test/std/containers/sequences/vector.bool/shrink_to_fit.pass.cpp
index f8bcee31964bbb..136b151efa29ef 100644
--- a/libcxx/test/std/containers/sequences/vector.bool/shrink_to_fit.pass.cpp
+++ b/libcxx/test/std/containers/sequences/vector.bool/shrink_to_fit.pass.cpp
@@ -55,7 +55,7 @@ struct increasing_allocator {
min_elements += 1000;
return std::allocator<T>{}.allocate_at_least(n);
}
- constexpr T* allocate(std::size_t n) { return allocate_at_least(n).ptr; }
+ constexpr T* allocate(std::size_t n) { return std::allocator<T>{}.allocate(n); }
constexpr void deallocate(T* p, std::size_t n) noexcept { std::allocator<T>{}.deallocate(p, n); }
};
diff --git a/libcxx/test/std/containers/sequences/vector/vector.capacity/shrink_to_fit.pass.cpp b/libcxx/test/std/containers/sequences/vector/vector.capacity/shrink_to_fit.pass.cpp
index e39afb2d48f0a0..97d67dac2baa8c 100644
--- a/libcxx/test/std/containers/sequences/vector/vector.capacity/shrink_to_fit.pass.cpp
+++ b/libcxx/test/std/containers/sequences/vector/vector.capacity/shrink_to_fit.pass.cpp
@@ -87,7 +87,7 @@ struct increasing_allocator {
min_elements += 1000;
return std::allocator<T>{}.allocate_at_least(n);
}
- constexpr T* allocate(std::size_t n) { return allocate_at_least(n).ptr; }
+ constexpr T* allocate(std::size_t n) { return std::allocator<T>{}.allocate(n); }
constexpr void deallocate(T* p, std::size_t n) noexcept { std::allocator<T>{}.deallocate(p, n); }
};
diff --git a/libcxx/test/std/strings/basic.string/string.capacity/shrink_to_fit.pass.cpp b/libcxx/test/std/strings/basic.string/string.capacity/shrink_to_fit.pass.cpp
index 6f5e43d1341f53..68360329308bab 100644
--- a/libcxx/test/std/strings/basic.string/string.capacity/shrink_to_fit.pass.cpp
+++ b/libcxx/test/std/strings/basic.string/string.capacity/shrink_to_fit.pass.cpp
@@ -79,8 +79,8 @@ struct increasing_allocator {
min_bytes += 1000;
return {static_cast<T*>(::operator new(allocation_amount)), allocation_amount / sizeof(T)};
}
- T* allocate(std::size_t n) { return allocate_at_least(n).ptr; }
- void deallocate(T* p, std::size_t) noexcept { ::operator delete(static_cast<void*>(p)); }
+ T* allocate(std::size_t n) { return std::allocator<T>{}.allocate(n); }
+ void deallocate(T* p, std::size_t) noexcept { std::allocator<T>{}.deallocate(p, n); }
};
template <typename T, typename U>
|
4044fbe
to
3402842
Compare
libcxx/test/std/containers/sequences/vector.bool/shrink_to_fit.pass.cpp
Outdated
Show resolved
Hide resolved
I feel like the reasoning is backwards here. AFAICT the only problem is that we're mismatching the |
@winner245 Actually, how did you come across this issue? Did our test suite fail with another Standard library somehow? |
Because I just realized that the same issue #95161 applies to I was trying to find a different way to solve the |
I personally do not like the current solution, because |
The intention of |
Actually, the tests failed with MSVC STL (or MSVC, the compiler). MSVC detects size mismatch of deallocation in constant evaluation. I guess we can't reliably perform over allocation in constant evaluation. (It seems that VCRuntime can detect size mismatch at run time.) |
I agree, and I don't see how this makes the allocator more conforming than it used to be. However, I do think the code makes more sense after the patch than it did before the patch, because implementing
constexpr std::allocation_result<T*> allocate_at_least(std::size_t n);
constexpr T* allocate(std::size_t n) {
auto res = allocate_at_least(n);
return res.ptr;
}
constexpr void deallocate(T* p, std::size_t n) noexcept {
// n may be wrong: it must match res.count which may not be the case if we used allocate_at_least above
std::allocator<T>{}.deallocate(p, n);
} Is that what you're saying? I would understand why the current code is a problem in that case, however http://eel.is/c++draft/allocator.members#10.1 says:
In other words, I think it's supposed to be just fine to call |
I'm not disagreeing here. The code is an improvement, but I don't think the commit message makes sense.
The calling code is fine. The mismatch between the Actually, I just noticed that the current code is still wrong, since there is still potentially the same mismatch when calling |
Thank you all for the very informative discussions here. I agree that the current solution for
|
Cpp17Allocator
requirements3402842
to
9d584f7
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks like we have agreement on the path forward. I left a few additional comments.
837b6d1
to
4696d88
Compare
min_elements += 1000; | ||
return std::allocator<T>{}.allocate_at_least(n); | ||
} | ||
constexpr T* allocate(std::size_t n) { return std::allocator<T>{}.allocate(n); } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we still need to make sure we always allocate an increasing amount of memory in allocate(...)
, otherwise this test allocator would not help catch the case where shrink_to_fit
is implemented incorrectly but it uses alloc.allocate
instead of alloc.allocate_at_least
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This makes sense to me. I just double checked the shrink_to_fit
implementation of vector
. Prior to C++23, it used allocate
, and since C++23 it uses allocate_at_least
. So by requiring both allocate
and allocate_at_least
to allocate the increasing amount of memory, we can make increasing_allocator
more useful for testing both implementations of shrink_to_fit
. I also made a little extra changes so that it can be used for both C++23 and pre-C++23.
4696d88
to
068904b
Compare
✅ With the latest revision this PR passed the C/C++ code formatter. |
d725c4f
to
bdd094f
Compare
#if TEST_STD_VER >= 23 | ||
return allocate_at_least(n).ptr; | ||
#else |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why don't we unconditionally call std::allocator<T>().allocate(n)
?
Also, we still run into the issue that we had before, which is that we're calling std::allocator<T>().allocate(n)
but calling std::allocator<T>().deallocate(ptr, m)
with a smaller value than we allocated. That violates allocator.members.
We can either do what @frederick-vs-ja suggested here (which TBH I don't fully understand why that makes the allocator ever-increasing), or we could try something like this: https://godbolt.org/z/P8PK6hvEj
template <typename T>
struct increasing_allocator {
using value_type = T;
std::size_t min_elements = 1000;
increasing_allocator() = default;
private:
tiny_map<void*, std::size_t> actual_sizes_;
public:
template <typename U>
constexpr increasing_allocator(const increasing_allocator<U>& other) noexcept
: min_elements(other.min_elements), actual_sizes_(other.actual_sizes_) {}
constexpr std::allocation_result<T*> allocate_at_least(std::size_t n) {
if (n < min_elements)
n = min_elements;
min_elements += 1000;
std::allocation_result<T*> result = std::allocator<T>().allocate_at_least(n);
actual_sizes_.add(result.ptr, n);
return result;
}
constexpr T* allocate(std::size_t n) {
if (n < min_elements)
n = min_elements;
min_elements += 1000;
T* result = std::allocator<T>().allocate(n);
actual_sizes_.add(result, n);
return result;
}
constexpr void deallocate(T* p, std::size_t n) noexcept {
auto actual_size = actual_sizes_.find(p);
std::allocator<T>().deallocate(p, actual_size->second);
actual_sizes_.remove(p);
}
friend constexpr bool operator==(increasing_allocator const& a, increasing_allocator const& b) noexcept {
return a.actual_sizes_ == b.actual_sizes_;
}
};
This is a lot more complicated but I think it works. If you agree with that direction, we could land this PR as-is and then tackle this additional change as a follow-up, since it can be done independently from this refactoring.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for your detailed feedback!
Why don't we unconditionally call
std::allocator<T>().allocate(n)
?
I agree that it's better to unconditionally call std::allocator<T>().allocate(n)
within increasing_allocator::allocate(n)
. I also think that we should allow only the allocate_at_least(n)
function of increasing_allocator
to allocate more memory than the requested amount n
, i.e., it actually allocates max(n, min_elements)
. However, I think it makes more sense for increasing_allocator::allocate(n)
to allocate exactly or effectively the amount n
, otherwise we will see a mismatch between the reported and the actual allocation counts, as shown in this example. This mismatch between the reported allocation and actual allocation could cause further misreporting in vector::capacity()
and make shrink_to_fit
malfunction, as shown in this demo.
The misreporting arises because the following code in the else
branch always reports __n
, which is correct only if __alloc.allocate(__n)
actually allocates exactly __n
. However, if our implementation allows increasing_allocator::allocate(n)
to allocate more than n
, we would trigger the misreporting problem.
template <class _Ap = _Alloc>
[[nodiscard]] _LIBCPP_HIDE_FROM_ABI static constexpr allocation_result<pointer, size_type>
allocate_at_least(_Ap& __alloc, size_type __n) {
if constexpr (requires { __alloc.allocate_at_least(__n); }) {
return __alloc.allocate_at_least(__n);
} else {
// This always reports n back. If allocate(n) allocates more than n, we have a report/actual mismatch
return {__alloc.allocate(__n), __n};
}
}
Therefore, allocate(n)
should allocate exactly n
and allocate_at_least(n)
should allocate at least n
, as their names suggest.
Also, we still run into the issue that we had before, which is that we're calling std::allocator().allocate(n) but calling std::allocator().deallocate(ptr, m) with a smaller value than we allocated. That violates allocator.members.
We can either do what @frederick-vs-ja suggested here (which TBH I don't fully understand why that makes the allocator ever-increasing), or we could try something like this: godbolt.org/z/P8PK6hvEj
I agree that the allocation/deallocation-size mismatch between allocate(n)/allocate_at_least(n)
and deallocate(p, m)
always exists because it completely depends on the usages of the functions. Users may provide mismatched m/n
values, regardless of our implementation. I think @frederick-vs-ja's idea provides a partial solution or it alleviates this issue by quantizing the allocation/deallocation sizes into discrete power-of-2 values. This allows for mismatches between n
and m
, as long as they are quantized to the same power of 2. However, completely irrelevant m, n
values yielding different equitizations may still cause mismatches.
To fully resolve the allocation/deallocation-size mismatch, we might need to record the actual allocation size during each allocation, similar to how you store the allocation size for each allocated memory p
. This way, regardless of what m
was provided during deallocate(p, m)
, we can always determine the actual allocation size from p
. Your code provides a neat implementation of this approach.
This is a lot more complicated but I think it works. If you agree with that direction, we could land this PR as-is and then tackle this additional change as a follow-up, since it can be done independently from this refactoring.
I agree it's getting a bit complicated but it seems worth proceeding. I agree with your direction. I'll keep the current refactoring in this PR as I really want the increasing_allocator
utility in a single location, so that other work (including my own ongoing work) can use this class without duplicating the code. Follow-up work to further improve this class can be done independently after this PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe another way to avoid the size mismatch problem is to use operator new and delete, which eliminates the need to manually specify the deallocation size:
template <typename T>
struct increasing_allocator {
using value_type = T;
std::size_t min_elements = 1000;
increasing_allocator() = default;
#if TEST_STD_VER >= 23
std::allocation_result<T*> allocate_at_least(std::size_t n) {
if (n < min_elements)
n = min_elements;
min_elements += 1000;
return {static_cast<T*>(::operator new(n * sizeof(T))), n};
}
#endif // TEST_STD_VER >= 23
T* allocate(std::size_t n) {
return static_cast<T*>(::operator new(n * sizeof(T)));
}
void deallocate(T* p, std::size_t) TEST_NOEXCEPT { ::operator delete(p); }
};
d6a3c9c
to
c27b6f6
Compare
c27b6f6
to
b4aefc5
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM. Let's tackle the allocation/deallocation size mismatch separately.
The
increasing_allocator<T>
class, originally introduced to testshrink_to_fit
forstd::vector
,vector<bool>
, andstd::basic_string
(#95161), has duplicated definitions across several test files. Given the potential utility of this class for capacity-related tests in various sequence containers, this PR proposes refactoring the definition ofincreasing_allocator<T>
into a single, reusable location.