[libc++] Don't assume alignment of empty ranges - #213752
Conversation
Iterators for empty ranges are sometimes implemented using a non-aligned sentinel value. Requiring proper alignment in that case causes a hardening failure. In reality, there is no benefit to requiring proper alignment for an empty range.
|
@llvm/pr-subscribers-libcxx Author: Louis Dionne (ldionne) ChangesIterators for empty ranges are sometimes implemented using a non-aligned sentinel value. Requiring proper alignment in that case causes a hardening failure. In reality, there is no benefit to requiring proper alignment for an empty range. Full diff: https://github.com/llvm/llvm-project/pull/213752.diff 2 Files Affected:
diff --git a/libcxx/include/__memory/valid_range.h b/libcxx/include/__memory/valid_range.h
index 32c98588fe39a..6edd68d53c49c 100644
--- a/libcxx/include/__memory/valid_range.h
+++ b/libcxx/include/__memory/valid_range.h
@@ -50,7 +50,9 @@ __is_valid_range(const _Tp* __first, const _Tp* __last) {
// This function allows the compiler to assume that [__first, __last) is a valid range as defined above.
//
// In practice, we only add explicit assumptions for bullets (1) and (3). These assumptions allow (currently only
-// clang-based compilers) to auto-vectorize algorithms that contain early returns.
+// clang-based compilers) to auto-vectorize algorithms that contain early returns. Note that we don't enforce
+// alignment assumptions for empty ranges since it is a common occurence to use unaligned sentinel values to
+// implement iterators of empty ranges.
template <class _Iter, class _Sent>
_LIBCPP_HIDE_FROM_ABI _LIBCPP_CONSTEXPR_SINCE_CXX14 void
__assume_valid_range([[__maybe_unused__]] _Iter&& __first, [[__maybe_unused__]] _Sent&& __last) {
@@ -60,10 +62,12 @@ __assume_valid_range([[__maybe_unused__]] _Iter&& __first, [[__maybe_unused__]]
_LIBCPP_ASSERT_INTERNAL(std::__is_valid_range(std::__to_address(__first), std::__to_address(__last)),
"Valid range assumption does not hold");
if (!__libcpp_is_constant_evaluated()) {
- using __value_type = typename iterator_traits<__remove_cvref_t<_Iter>>::value_type;
- __builtin_assume_dereferenceable(std::__to_address(__first), (__last - __first) * sizeof(__value_type));
- (void)std::__assume_aligned<_LIBCPP_ALIGNOF(__value_type)>(std::__to_address(__first));
- (void)std::__assume_aligned<_LIBCPP_ALIGNOF(__value_type)>(std::__to_address(__last));
+ if (__first != __last) {
+ using __value_type = typename iterator_traits<__remove_cvref_t<_Iter>>::value_type;
+ __builtin_assume_dereferenceable(std::__to_address(__first), (__last - __first) * sizeof(__value_type));
+ (void)std::__assume_aligned<_LIBCPP_ALIGNOF(__value_type)>(std::__to_address(__first));
+ (void)std::__assume_aligned<_LIBCPP_ALIGNOF(__value_type)>(std::__to_address(__last));
+ }
}
}
#endif
diff --git a/libcxx/test/libcxx/algorithms/unaligned_empty_range.pass.cpp b/libcxx/test/libcxx/algorithms/unaligned_empty_range.pass.cpp
new file mode 100644
index 0000000000000..8cd9bcd67f870
--- /dev/null
+++ b/libcxx/test/libcxx/algorithms/unaligned_empty_range.pass.cpp
@@ -0,0 +1,82 @@
+//===----------------------------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+// Some algorithms make alignment assumptions for optimization purposes. However, it is
+// fairly common for data structures to implement iterators for empty ranges using
+// sentinel values, which may not be properly aligned. This test ensures that we don't
+// make alignment assumptions for empty ranges, which would break that use case.
+
+// UNSUPPORTED: c++03
+
+#include <algorithm>
+#include <cassert>
+#include <cstdint>
+#include <ranges>
+
+#include "test_macros.h"
+
+// Returns a pointer that doesn't point to any object at all and that is not suitably
+// aligned for T. This is the sort of sentinel value that user code sometimes uses to
+// represent an empty range.
+template <class T>
+T* unaligned_sentinel() {
+ return reinterpret_cast<T*>(static_cast<std::uintptr_t>(1));
+}
+
+template <class T>
+void test() {
+ T* first = unaligned_sentinel<T>();
+ T* last = first;
+ T value{};
+ auto pred = [](T const&) { return true; };
+
+ assert(std::find(first, last, value) == last);
+ assert(std::find_if(first, last, pred) == last);
+ assert(std::find_if_not(first, last, pred) == last);
+ assert(std::any_of(first, last, pred) == false);
+ assert(std::all_of(first, last, pred) == true);
+ assert(std::none_of(first, last, pred) == true);
+ assert(std::remove(first, last, value) == last);
+ assert(std::remove_if(first, last, pred) == last);
+
+#if TEST_STD_VER >= 20
+ // (iterator, sentinel) overloads
+ assert(std::ranges::find(first, last, value) == last);
+ assert(std::ranges::find_if(first, last, pred) == last);
+ assert(std::ranges::find_if_not(first, last, pred) == last);
+ assert(std::ranges::any_of(first, last, pred) == false);
+ assert(std::ranges::all_of(first, last, pred) == true);
+ assert(std::ranges::none_of(first, last, pred) == true);
+ assert(std::ranges::remove(first, last, value).begin() == last);
+ assert(std::ranges::remove_if(first, last, pred).begin() == last);
+
+ // (range) overloads
+ std::ranges::subrange range(first, last);
+ assert(std::ranges::find(range, value) == last);
+ assert(std::ranges::find_if(range, pred) == last);
+ assert(std::ranges::find_if_not(range, pred) == last);
+ assert(std::ranges::any_of(range, pred) == false);
+ assert(std::ranges::all_of(range, pred) == true);
+ assert(std::ranges::none_of(range, pred) == true);
+ assert(std::ranges::remove(range, value).begin() == last);
+ assert(std::ranges::remove_if(range, pred).begin() == last);
+#endif
+}
+
+struct alignas(16) Overaligned {
+ int value = 0;
+ friend bool operator==(Overaligned const& x, Overaligned const& y) { return x.value == y.value; }
+};
+
+int main(int, char**) {
+ test<int>();
+ test<double>(); // not trivially equality comparable
+ test<Overaligned>();
+
+ return 0;
+}
|
| __builtin_assume_dereferenceable(std::__to_address(__first), (__last - __first) * sizeof(__value_type)); | ||
| (void)std::__assume_aligned<_LIBCPP_ALIGNOF(__value_type)>(std::__to_address(__first)); | ||
| (void)std::__assume_aligned<_LIBCPP_ALIGNOF(__value_type)>(std::__to_address(__last)); | ||
| if (__first != __last) { |
There was a problem hiding this comment.
I suspect this might throw off compiler optimizations. Another approach for this PR would be to instead still assume valid alignment for all ranges (including empty ranges), but early-exit from std::__find_if before we make that assumption.
There was a problem hiding this comment.
I agree with the early-exit approach in std::__find_if think it might be cleaner
|
/libcxx-bot benchmark libcxx/test/benchmarks/algorithms/nonmodifying/find.bench.cpp libcxx/test/benchmarks/algorithms/nonmodifying/any_all_none_of.bench.cpp
Benchmark results for Linux x86_64:Benchmark results for macOS 26.5 arm64: |
philnik777
left a comment
There was a problem hiding this comment.
Kinda related: Should we assert alignment (possibly only in debug)? This seems like the kind of thing that will bite users in the end.
Iterators for empty ranges are sometimes implemented using a non-aligned sentinel value. Requiring proper alignment in that case causes a hardening failure. In reality, there is no benefit to requiring proper alignment for an empty range.