[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