[libcxx-commits] [libcxx] Libcxx nth element small ranges (PR #215698)
via libcxx-commits
libcxx-commits at lists.llvm.org
Tue Aug 11 16:49:06 PDT 2026
llvmorg-github-actions[bot] wrote:
<!--LLVM PR SUMMARY COMMENT-->
@llvm/pr-subscribers-libcxx
Author: Adib Pathan (adxb-pxthxn)
<details>
<summary>Changes</summary>
## Summary
- Specialize `std::nth_element` for ranges of at most 5 elements by dispatching to the existing `__sort3` / `__sort4` / `__sort5` sorting networks (a full sort trivially satisfies the `nth_element` postconditions).
- Perform that dispatch in the inlinable wrapper behind `__builtin_constant_p`, so callers with a compile-time window size (e.g. median-of-3 / median-of-5 filters) constant-fold to a branchless network with no call into the general algorithm. Runtime small ranges still benefit via the same helper inside `__nth_element`.
- Make `__sort4` / `__sort5` constexpr so the new paths work under C++20 constexpr `nth_element`.
- Add exhaustive permutation/duplicate tests for sizes 0–8, and extend the `nth_element` benchmark with fixed-size median-of-3/5 windows.
testing assisted by fable.
Fixes #<!-- -->214238
## Test plan
- [x] Exhaustive lit test: all permutations of sizes 0–8 (every `nth`, `less`/`greater`), duplicate bit-patterns, `random_access_iterator` + pointers, constexpr sizes 3/4/5
- [x] Existing `alg.nth.element` + broader `alg.sorting` / robustness lit suites
- [x] `nth_element.bench.cpp` builds and runs with the new median-of-3/5 cases
- [ ] Confirm LLVM CI across standard modes
---
Full diff: https://github.com/llvm/llvm-project/pull/215698.diff
5 Files Affected:
- (modified) libcxx/docs/ReleaseNotes/24.rst (+4)
- (modified) libcxx/include/__algorithm/nth_element.h (+49-15)
- (modified) libcxx/include/__algorithm/sort.h (+4-4)
- (modified) libcxx/test/benchmarks/algorithms/sorting/nth_element.bench.cpp (+49)
- (added) libcxx/test/std/algorithms/alg.sorting/alg.nth.element/nth_element.exhaustive.pass.cpp (+123)
``````````diff
diff --git a/libcxx/docs/ReleaseNotes/24.rst b/libcxx/docs/ReleaseNotes/24.rst
index e9a36ae94392f..1640bfcd3eb86 100644
--- a/libcxx/docs/ReleaseNotes/24.rst
+++ b/libcxx/docs/ReleaseNotes/24.rst
@@ -51,6 +51,10 @@ Implemented Papers
Improvements and New Features
-----------------------------
+- ``std::nth_element`` now uses sorting networks for ranges with at most 5 elements, which makes it significantly
+ faster on small ranges. In particular, when the size of the range is known at compile time (as in a median-of-5
+ filter), the algorithm now compiles down to a branchless sorting network.
+
Deprecations and Removals
-------------------------
diff --git a/libcxx/include/__algorithm/nth_element.h b/libcxx/include/__algorithm/nth_element.h
index da748d7255aba..53aca0b55b6a4 100644
--- a/libcxx/include/__algorithm/nth_element.h
+++ b/libcxx/include/__algorithm/nth_element.h
@@ -28,6 +28,43 @@ _LIBCPP_PUSH_MACROS
_LIBCPP_BEGIN_NAMESPACE_STD
+// Handles ranges of at most 5 elements by sorting them entirely, which trivially satisfies the
+// nth_element postconditions. Sorting networks are the most efficient way to handle such small
+// ranges, and this function boils down to one of them when the size of the range is known at
+// compile time. Returns false if the range was too large to be handled.
+template <class _AlgPolicy, class _Compare, class _RandomAccessIterator>
+_LIBCPP_HIDE_FROM_ABI _LIBCPP_CONSTEXPR_SINCE_CXX14 bool
+__nth_element_small(_RandomAccessIterator __first, _RandomAccessIterator __last, _Compare __comp) {
+ using _Ops = _IterOps<_AlgPolicy>;
+ typedef typename iterator_traits<_RandomAccessIterator>::difference_type difference_type;
+ switch (__last - __first) {
+ case 0:
+ case 1:
+ return true;
+ case 2:
+ if (__comp(*--__last, *__first))
+ _Ops::iter_swap(__first, __last);
+ return true;
+ case 3:
+ std::__sort3<_AlgPolicy, _Compare>(__first, __first + difference_type(1), --__last, __comp);
+ return true;
+ case 4:
+ std::__sort4<_AlgPolicy, _Compare>(
+ __first, __first + difference_type(1), __first + difference_type(2), --__last, __comp);
+ return true;
+ case 5:
+ std::__sort5<_AlgPolicy, _Compare>(
+ __first,
+ __first + difference_type(1),
+ __first + difference_type(2),
+ __first + difference_type(3),
+ --__last,
+ __comp);
+ return true;
+ }
+ return false;
+}
+
template <class _Compare, class _RandomAccessIterator>
_LIBCPP_HIDE_FROM_ABI _LIBCPP_CONSTEXPR_SINCE_CXX14 bool __nth_element_find_guard(
_RandomAccessIterator& __i, _RandomAccessIterator& __j, _RandomAccessIterator __m, _Compare __comp) {
@@ -55,21 +92,9 @@ __nth_element(
while (true) {
if (__nth == __last)
return;
- difference_type __len = __last - __first;
- switch (__len) {
- case 0:
- case 1:
- return;
- case 2:
- if (__comp(*--__last, *__first))
- _Ops::iter_swap(__first, __last);
- return;
- case 3: {
- _RandomAccessIterator __m = __first;
- std::__sort3<_AlgPolicy, _Compare>(__first, ++__m, --__last, __comp);
+ if (std::__nth_element_small<_AlgPolicy, _Compare>(__first, __last, __comp))
return;
- }
- }
+ difference_type __len = __last - __first;
if (__len <= __limit) {
std::__selection_sort<_AlgPolicy, _Compare>(__first, __last, __comp);
return;
@@ -234,7 +259,16 @@ inline _LIBCPP_HIDE_FROM_ABI _LIBCPP_CONSTEXPR_SINCE_CXX20 void __nth_element_im
std::__debug_randomize_range<_AlgPolicy>(__first, __last);
- std::__nth_element<_AlgPolicy, __comp_ref_type<_Compare> >(__first, __nth, __last, __comp);
+ // When the size of the range is known at compile time (e.g. in a median-of-5 filter), this
+ // dispatch constant-folds to the appropriate sorting network. Otherwise, the check disappears
+ // entirely and we fall through to the general algorithm, which handles small ranges itself.
+ // Note that __builtin_constant_p is only resolved after inlining, so this catches callers that
+ // pass a constant size even though it is not a constant expression here.
+ bool __small_constant_size =
+ __builtin_constant_p(__last - __first) &&
+ std::__nth_element_small<_AlgPolicy, __comp_ref_type<_Compare> >(__first, __last, __comp);
+ if (!__small_constant_size)
+ std::__nth_element<_AlgPolicy, __comp_ref_type<_Compare> >(__first, __nth, __last, __comp);
std::__debug_randomize_range<_AlgPolicy>(__first, __nth);
if (__nth != __last) {
diff --git a/libcxx/include/__algorithm/sort.h b/libcxx/include/__algorithm/sort.h
index 0a936db78e5fd..4b450e7dc9194 100644
--- a/libcxx/include/__algorithm/sort.h
+++ b/libcxx/include/__algorithm/sort.h
@@ -140,7 +140,7 @@ template <class,
class _Compare,
class _RandomAccessIterator,
__enable_if_t<__use_branchless_sort<_Compare, _RandomAccessIterator>, int> = 0>
-inline _LIBCPP_HIDE_FROM_ABI void
+inline _LIBCPP_HIDE_FROM_ABI _LIBCPP_CONSTEXPR_SINCE_CXX14 void
__sort4(_RandomAccessIterator __x1,
_RandomAccessIterator __x2,
_RandomAccessIterator __x3,
@@ -157,7 +157,7 @@ template <class _AlgPolicy,
class _Compare,
class _RandomAccessIterator,
__enable_if_t<!__use_branchless_sort<_Compare, _RandomAccessIterator>, int> = 0>
-inline _LIBCPP_HIDE_FROM_ABI void
+inline _LIBCPP_HIDE_FROM_ABI _LIBCPP_CONSTEXPR_SINCE_CXX14 void
__sort4(_RandomAccessIterator __x1,
_RandomAccessIterator __x2,
_RandomAccessIterator __x3,
@@ -182,7 +182,7 @@ template <class _AlgPolicy,
class _Compare,
class _RandomAccessIterator,
__enable_if_t<__use_branchless_sort<_Compare, _RandomAccessIterator>, int> = 0>
-inline _LIBCPP_HIDE_FROM_ABI void
+inline _LIBCPP_HIDE_FROM_ABI _LIBCPP_CONSTEXPR_SINCE_CXX14 void
__sort5(_RandomAccessIterator __x1,
_RandomAccessIterator __x2,
_RandomAccessIterator __x3,
@@ -201,7 +201,7 @@ template <class _AlgPolicy,
class _Compare,
class _RandomAccessIterator,
__enable_if_t<!__use_branchless_sort<_Compare, _RandomAccessIterator>, int> = 0>
-inline _LIBCPP_HIDE_FROM_ABI void
+inline _LIBCPP_HIDE_FROM_ABI _LIBCPP_CONSTEXPR_SINCE_CXX14 void
__sort5(_RandomAccessIterator __x1,
_RandomAccessIterator __x2,
_RandomAccessIterator __x3,
diff --git a/libcxx/test/benchmarks/algorithms/sorting/nth_element.bench.cpp b/libcxx/test/benchmarks/algorithms/sorting/nth_element.bench.cpp
index 4c3e8115def30..d210e514a35fd 100644
--- a/libcxx/test/benchmarks/algorithms/sorting/nth_element.bench.cpp
+++ b/libcxx/test/benchmarks/algorithms/sorting/nth_element.bench.cpp
@@ -84,6 +84,55 @@ int main(int argc, char** argv) {
register_bm(support::single_element_data<int>, "repeated");
}
+ // Benchmark std::nth_element on many independent fixed-size windows. This models
+ // the median-of-3 / median-of-5 filter use case from #214238, where the window
+ // size is a compile-time constant and the specialized sorting-network path is
+ // expected to constant-fold away the general algorithm.
+ {
+ auto bm = []<std::size_t Window>(std::string name, auto generate_data) {
+ benchmark::RegisterBenchmark(
+ name,
+ [generate_data](auto& st) {
+ std::size_t const num_windows = st.range(0);
+ constexpr std::size_t BatchSize = 32;
+ std::vector<int> flat = generate_data(num_windows * Window);
+ std::array<std::vector<std::array<int, Window>>, BatchSize> batches;
+ for (std::size_t b = 0; b != BatchSize; ++b) {
+ batches[b].resize(num_windows);
+ for (std::size_t i = 0; i != num_windows; ++i) {
+ std::copy_n(flat.begin() + static_cast<std::ptrdiff_t>(i * Window), Window, batches[b][i].begin());
+ }
+ }
+
+ constexpr std::size_t Median = Window / 2;
+ while (st.KeepRunningBatch(BatchSize)) {
+ for (std::size_t b = 0; b != BatchSize; ++b) {
+ for (std::size_t i = 0; i != num_windows; ++i) {
+ benchmark::DoNotOptimize(batches[b][i]);
+ std::nth_element(batches[b][i].begin(), batches[b][i].begin() + Median, batches[b][i].end());
+ benchmark::DoNotOptimize(batches[b][i]);
+ }
+ }
+
+ st.PauseTiming();
+ for (std::size_t b = 0; b != BatchSize; ++b) {
+ for (std::size_t i = 0; i != num_windows; ++i) {
+ std::copy_n(flat.begin() + static_cast<std::ptrdiff_t>(i * Window), Window, batches[b][i].begin());
+ }
+ }
+ st.ResumeTiming();
+ }
+ })
+ ->Arg(1024)
+ ->Arg(8192);
+ };
+
+ bm.operator()<3>("std::nth_element(median-of-3 windows) (shuffled)", support::shuffled_data<int>);
+ bm.operator()<5>("std::nth_element(median-of-5 windows) (shuffled)", support::shuffled_data<int>);
+ bm.operator()<3>("std::nth_element(median-of-3 windows) (ascending)", support::ascending_sorted_data<int>);
+ bm.operator()<5>("std::nth_element(median-of-5 windows) (ascending)", support::ascending_sorted_data<int>);
+ }
+
benchmark::Initialize(&argc, argv);
benchmark::RunSpecifiedBenchmarks();
benchmark::Shutdown();
diff --git a/libcxx/test/std/algorithms/alg.sorting/alg.nth.element/nth_element.exhaustive.pass.cpp b/libcxx/test/std/algorithms/alg.sorting/alg.nth.element/nth_element.exhaustive.pass.cpp
new file mode 100644
index 0000000000000..b747a18be8146
--- /dev/null
+++ b/libcxx/test/std/algorithms/alg.sorting/alg.nth.element/nth_element.exhaustive.pass.cpp
@@ -0,0 +1,123 @@
+//===----------------------------------------------------------------------===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+
+// <algorithm>
+
+// Exhaustively check std::nth_element on all permutations of small ranges.
+// This specifically covers the size-0..5 sorting-network specializations and the
+// size-6..7 selection-sort fallback.
+
+#include <algorithm>
+#include <cassert>
+#include <cstddef>
+#include <functional>
+#include <numeric>
+#include <vector>
+
+#include "test_iterators.h"
+#include "test_macros.h"
+
+template <class Iter, class Compare>
+void check_postcondition(Iter first, Iter nth, Iter last, Compare comp) {
+ if (nth == last)
+ return;
+ for (Iter i = first; i != nth; ++i)
+ assert(!comp(*nth, *i));
+ for (Iter i = nth; i != last; ++i)
+ assert(!comp(*i, *nth));
+}
+
+template <class Iter, class Compare>
+void test_permutations(std::vector<int> values, Compare comp) {
+ std::size_t const size = values.size();
+ std::vector<int> expected = values;
+ std::sort(expected.begin(), expected.end(), comp);
+
+ do {
+ for (std::size_t nth = 0; nth <= size; ++nth) {
+ std::vector<int> v = values;
+ std::nth_element(Iter(v.data()), Iter(v.data() + nth), Iter(v.data() + size), comp);
+ if (nth != size)
+ assert(v[nth] == expected[nth]);
+ assert(std::is_permutation(v.begin(), v.end(), values.begin()));
+ check_postcondition(Iter(v.data()), Iter(v.data() + nth), Iter(v.data() + size), comp);
+ }
+ } while (std::next_permutation(values.begin(), values.end()));
+}
+
+template <class Iter>
+void test_distinct() {
+ for (int size = 0; size <= 8; ++size) {
+ std::vector<int> values(static_cast<std::size_t>(size));
+ std::iota(values.begin(), values.end(), 0);
+ test_permutations<Iter>(values, std::less<int>());
+ test_permutations<Iter>(values, std::greater<int>());
+ }
+}
+
+// Exercise equal-element paths with every bit pattern of 0/1 for sizes 1..8.
+template <class Iter>
+void test_duplicates() {
+ for (int size = 1; size <= 8; ++size) {
+ for (int mask = 0; mask < (1 << size); ++mask) {
+ std::vector<int> values(static_cast<std::size_t>(size));
+ for (int i = 0; i < size; ++i)
+ values[static_cast<std::size_t>(i)] = (mask >> i) & 1;
+
+ std::vector<int> expected = values;
+ std::sort(expected.begin(), expected.end());
+ for (int nth = 0; nth < size; ++nth) {
+ std::vector<int> v = values;
+ std::nth_element(Iter(v.data()), Iter(v.data() + nth), Iter(v.data() + size));
+ assert(v[static_cast<std::size_t>(nth)] == expected[static_cast<std::size_t>(nth)]);
+ check_postcondition(Iter(v.data()), Iter(v.data() + nth), Iter(v.data() + size), std::less<int>());
+ }
+ }
+ }
+}
+
+#if TEST_STD_VER >= 20
+constexpr bool test_constexpr_small_ranges() {
+ // Sizes 3, 4 and 5 hit the specialized sorting-network dispatch.
+ {
+ int a[] = {2, 0, 1};
+ std::nth_element(a, a + 1, a + 3);
+ if (a[1] != 1)
+ return false;
+ }
+ {
+ int a[] = {3, 0, 2, 1};
+ std::nth_element(a, a + 2, a + 4);
+ if (a[2] != 2)
+ return false;
+ }
+ {
+ int a[] = {4, 2, 0, 3, 1};
+ std::nth_element(a, a + 2, a + 5);
+ if (a[2] != 2)
+ return false;
+ }
+ {
+ int a[] = {4, 2, 0, 3, 1};
+ std::nth_element(a, a + 2, a + 5, std::greater<>());
+ if (a[2] != 2)
+ return false;
+ }
+ return true;
+}
+static_assert(test_constexpr_small_ranges());
+#endif
+
+int main(int, char**) {
+ test_distinct<int*>();
+ test_distinct<random_access_iterator<int*> >();
+ test_duplicates<int*>();
+ test_duplicates<random_access_iterator<int*> >();
+
+ return 0;
+}
``````````
</details>
https://github.com/llvm/llvm-project/pull/215698
More information about the libcxx-commits
mailing list