[libcxx-commits] [libcxx] [libc++] Rewrite the set_intersection benchmark without CartesianBenchmarks (PR #214302)

Louis Dionne via libcxx-commits libcxx-commits at lists.llvm.org
Fri Aug 7 05:49:04 PDT 2026


https://github.com/ldionne updated https://github.com/llvm/llvm-project/pull/214302

>From bbd9072440af45dc2db2e62d1de8d5138885c981 Mon Sep 17 00:00:00 2001
From: Louis Dionne <ldionne.2 at gmail.com>
Date: Tue, 4 Aug 2026 16:07:56 -0400
Subject: [PATCH 1/2] [libc++] Rewrite the set_intersection benchmark without
 CartesianBenchmarks

The set_intersection benchmark was taking around 15% of the time of the
whole test suite by itself. It generated 1296 test cases including many
that were irrelevant. It also nested two mutually exclusive GoogleBenchmark
iteration APIs (a for-loop on state and KeepRunningBatch), leading to
invalid results anyway.

This patch rewrites the benchmark with manually curated cases instead.

Addresses #120763

Assisted by Claude
---
 libcxx/test/benchmarks/algorithms/common.h    | 247 ------------------
 .../algorithms/set_intersection.bench.cpp     | 186 -------------
 .../sorting/set_intersection.bench.cpp        | 206 +++++++++++++++
 3 files changed, 206 insertions(+), 433 deletions(-)
 delete mode 100644 libcxx/test/benchmarks/algorithms/common.h
 delete mode 100644 libcxx/test/benchmarks/algorithms/set_intersection.bench.cpp
 create mode 100644 libcxx/test/benchmarks/algorithms/sorting/set_intersection.bench.cpp

diff --git a/libcxx/test/benchmarks/algorithms/common.h b/libcxx/test/benchmarks/algorithms/common.h
deleted file mode 100644
index 43131a4c9c922..0000000000000
--- a/libcxx/test/benchmarks/algorithms/common.h
+++ /dev/null
@@ -1,247 +0,0 @@
-//===----------------------------------------------------------------------===//
-//
-// 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
-//
-//===----------------------------------------------------------------------===//
-
-#ifndef LIBCXX_ALGORITHMS_COMMON_H
-#define LIBCXX_ALGORITHMS_COMMON_H
-
-#include <algorithm>
-#include <numeric>
-#include <tuple>
-#include <vector>
-
-#include "../CartesianBenchmarks.h"
-#include "../GenerateInput.h"
-
-enum class ValueType { Uint32, Uint64, Pair, Tuple, String, Float };
-struct AllValueTypes : EnumValuesAsTuple<AllValueTypes, ValueType, 6> {
-  static constexpr const char* Names[] = {
-      "uint32", "uint64", "pair<uint32, uint32>", "tuple<uint32, uint64, uint32>", "string", "float"};
-};
-
-using Types =
-    std::tuple<uint32_t,
-               uint64_t,
-               std::pair<uint32_t, uint32_t>,
-               std::tuple<uint32_t, uint64_t, uint32_t>,
-               std::string,
-               float>;
-
-template <class V>
-using Value = std::tuple_element_t<(int)V::value, Types>;
-
-enum class Order {
-  Random,
-  Ascending,
-  Descending,
-  SingleElement,
-  PipeOrgan,
-  Heap,
-  QuickSortAdversary,
-};
-struct AllOrders : EnumValuesAsTuple<AllOrders, Order, 7> {
-  static constexpr const char* Names[] = {
-      "Random", "Ascending", "Descending", "SingleElement", "PipeOrgan", "Heap", "QuickSortAdversary"};
-};
-
-// fillAdversarialQuickSortInput fills the input vector with N int-like values.
-// These values are arranged in such a way that they would invoke O(N^2)
-// behavior on any quick sort implementation that satisifies certain conditions.
-// Details are available in the following paper:
-// "A Killer Adversary for Quicksort", M. D. McIlroy, Software-Practice &
-// Experience Volume 29 Issue 4 April 10, 1999 pp 341-344.
-// https://dl.acm.org/doi/10.5555/311868.311871.
-template <class T>
-void fillAdversarialQuickSortInput(T& V, size_t N) {
-  assert(N > 0);
-  // If an element is equal to gas, it indicates that the value of the element
-  // is still to be decided and may change over the course of time.
-  const unsigned int gas = N - 1;
-  V.resize(N);
-  for (unsigned int i = 0; i < N; ++i) {
-    V[i] = gas;
-  }
-  // Candidate for the pivot position.
-  int candidate = 0;
-  int nsolid    = 0;
-  // Populate all positions in the generated input to gas.
-  std::vector<int> ascVals(V.size());
-  // Fill up with ascending values from 0 to V.size()-1.  These will act as
-  // indices into V.
-  std::iota(ascVals.begin(), ascVals.end(), 0);
-  std::sort(ascVals.begin(), ascVals.end(), [&](int x, int y) {
-    if (V[x] == gas && V[y] == gas) {
-      // We are comparing two inputs whose value is still to be decided.
-      if (x == candidate) {
-        V[x] = nsolid++;
-      } else {
-        V[y] = nsolid++;
-      }
-    }
-    if (V[x] == gas) {
-      candidate = x;
-    } else if (V[y] == gas) {
-      candidate = y;
-    }
-    return V[x] < V[y];
-  });
-}
-
-template <typename T>
-void fillValues(std::vector<T>& V, size_t N, Order O) {
-  if (O == Order::SingleElement) {
-    V.resize(N, 0);
-  } else if (O == Order::QuickSortAdversary) {
-    fillAdversarialQuickSortInput(V, N);
-  } else {
-    while (V.size() < N)
-      V.push_back(V.size());
-  }
-}
-
-template <typename T>
-void fillValues(std::vector<std::pair<T, T> >& V, size_t N, Order O) {
-  if (O == Order::SingleElement) {
-    V.resize(N, std::make_pair(0, 0));
-  } else {
-    while (V.size() < N)
-      // Half of array will have the same first element.
-      if (V.size() % 2) {
-        V.push_back(std::make_pair(V.size(), V.size()));
-      } else {
-        V.push_back(std::make_pair(0, V.size()));
-      }
-  }
-}
-
-template <typename T1, typename T2, typename T3>
-void fillValues(std::vector<std::tuple<T1, T2, T3> >& V, size_t N, Order O) {
-  if (O == Order::SingleElement) {
-    V.resize(N, std::make_tuple(0, 0, 0));
-  } else {
-    while (V.size() < N)
-      // One third of array will have the same first element.
-      // One third of array will have the same first element and the same second element.
-      switch (V.size() % 3) {
-      case 0:
-        V.push_back(std::make_tuple(V.size(), V.size(), V.size()));
-        break;
-      case 1:
-        V.push_back(std::make_tuple(0, V.size(), V.size()));
-        break;
-      case 2:
-        V.push_back(std::make_tuple(0, 0, V.size()));
-        break;
-      }
-  }
-}
-
-inline void fillValues(std::vector<std::string>& V, size_t N, Order O) {
-  if (O == Order::SingleElement) {
-    V.resize(N, getRandomString(64));
-  } else {
-    while (V.size() < N)
-      V.push_back(getRandomString(64));
-  }
-}
-
-template <class T>
-void sortValues(T& V, Order O) {
-  switch (O) {
-  case Order::Random: {
-    std::random_device R;
-    std::mt19937 M(R());
-    std::shuffle(V.begin(), V.end(), M);
-    break;
-  }
-  case Order::Ascending:
-    std::sort(V.begin(), V.end());
-    break;
-  case Order::Descending:
-    std::sort(V.begin(), V.end(), std::greater<>());
-    break;
-  case Order::SingleElement:
-    // Nothing to do
-    break;
-  case Order::PipeOrgan:
-    std::sort(V.begin(), V.end());
-    std::reverse(V.begin() + V.size() / 2, V.end());
-    break;
-  case Order::Heap:
-    std::make_heap(V.begin(), V.end());
-    break;
-  case Order::QuickSortAdversary:
-    // Nothing to do
-    break;
-  }
-}
-
-constexpr size_t TestSetElements =
-#if !TEST_HAS_FEATURE(memory_sanitizer)
-    1 << 18;
-#else
-    1 << 14;
-#endif
-
-template <class ValueType>
-std::vector<std::vector<Value<ValueType> > > makeOrderedValues(size_t N, Order O) {
-  std::vector<std::vector<Value<ValueType> > > Ret;
-  const size_t NumCopies = std::max(size_t{1}, TestSetElements / N);
-  Ret.resize(NumCopies);
-  for (auto& V : Ret) {
-    fillValues(V, N, O);
-    sortValues(V, O);
-  }
-  return Ret;
-}
-
-template <class T, class U>
-TEST_ALWAYS_INLINE void resetCopies(benchmark::State& state, T& Copies, U& Orig) {
-  state.PauseTiming();
-  for (auto& Copy : Copies)
-    Copy = Orig;
-  state.ResumeTiming();
-}
-
-enum class BatchSize {
-  CountElements,
-  CountBatch,
-};
-
-template <class ValueType, class F>
-void runOpOnCopies(benchmark::State& state, size_t Quantity, Order O, BatchSize Count, F Body) {
-  auto Copies = makeOrderedValues<ValueType>(Quantity, O);
-  auto Orig   = Copies;
-
-  const size_t Batch = Count == BatchSize::CountElements ? Copies.size() * Quantity : Copies.size();
-  while (state.KeepRunningBatch(Batch)) {
-    for (auto& Copy : Copies) {
-      Body(Copy);
-      benchmark::DoNotOptimize(Copy);
-    }
-    state.PauseTiming();
-    Copies = Orig;
-    state.ResumeTiming();
-  }
-}
-
-const std::vector<size_t> Quantities = {
-    1 << 0,
-    1 << 2,
-    1 << 4,
-    1 << 6,
-    1 << 8,
-    1 << 10,
-    1 << 14,
-// Running each benchmark in parallel consumes too much memory with MSAN
-// and can lead to the test process being killed.
-#if !TEST_HAS_FEATURE(memory_sanitizer)
-    1 << 18
-#endif
-};
-
-#endif // LIBCXX_ALGORITHMS_COMMON_H
diff --git a/libcxx/test/benchmarks/algorithms/set_intersection.bench.cpp b/libcxx/test/benchmarks/algorithms/set_intersection.bench.cpp
deleted file mode 100644
index 40292179781ee..0000000000000
--- a/libcxx/test/benchmarks/algorithms/set_intersection.bench.cpp
+++ /dev/null
@@ -1,186 +0,0 @@
-//===----------------------------------------------------------------------===//
-//
-// 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
-//
-//===----------------------------------------------------------------------===//
-
-// UNSUPPORTED: c++03, c++11, c++14, c++17
-
-#include <algorithm>
-#include <cstdlib>
-#include <iterator>
-#include <set>
-#include <vector>
-
-#include "common.h"
-#include "test_iterators.h"
-
-namespace {
-
-// types of containers we'll want to test, covering interesting iterator types
-struct VectorContainer {
-  template <typename... Args>
-  using type = std::vector<Args...>;
-
-  static constexpr const char* Name = "Vector";
-};
-
-struct SetContainer {
-  template <typename... Args>
-  using type = std::set<Args...>;
-
-  static constexpr const char* Name = "Set";
-};
-
-using AllContainerTypes = std::tuple<VectorContainer, SetContainer>;
-
-// set_intersection performance may depend on where matching values lie
-enum class OverlapPosition {
-  None,
-  Front,
-  // performance-wise, matches at the back are identical to ones at the front
-  Interlaced,
-};
-
-struct AllOverlapPositions : EnumValuesAsTuple<AllOverlapPositions, OverlapPosition, 3> {
-  static constexpr const char* Names[] = {"None", "Front", "Interlaced"};
-};
-
-// forward_iterator wrapping which, for each increment, moves the underlying iterator forward Stride elements
-template <typename Wrapped>
-struct StridedFwdIt {
-  Wrapped base_;
-  unsigned stride_;
-
-  using iterator_category = std::forward_iterator_tag;
-  using difference_type   = typename Wrapped::difference_type;
-  using value_type        = typename Wrapped::value_type;
-  using pointer           = typename Wrapped::pointer;
-  using reference         = typename Wrapped::reference;
-
-  StridedFwdIt(Wrapped base, unsigned stride) : base_(base), stride_(stride) { assert(stride_ != 0); }
-
-  StridedFwdIt operator++() {
-    for (unsigned i = 0; i < stride_; ++i)
-      ++base_;
-    return *this;
-  }
-  StridedFwdIt operator++(int) {
-    auto tmp = *this;
-    ++*this;
-    return tmp;
-  }
-  value_type& operator*() { return *base_; }
-  const value_type& operator*() const { return *base_; }
-  value_type& operator->() { return *base_; }
-  const value_type& operator->() const { return *base_; }
-  bool operator==(const StridedFwdIt& o) const { return base_ == o.base_; }
-  bool operator!=(const StridedFwdIt& o) const { return !operator==(o); }
-};
-template <typename Wrapped>
-StridedFwdIt(Wrapped, unsigned) -> StridedFwdIt<Wrapped>;
-
-template <typename T>
-std::vector<T> getVectorOfRandom(size_t N) {
-  std::vector<T> v;
-  fillValues(v, N, Order::Random);
-  sortValues(v, Order::Random);
-  return std::vector<T>(v);
-}
-
-// Realistically, data won't all be nicely contiguous in a container,
-// we'll go through some effort to ensure that it's shuffled through memory
-// this is especially important for containers with non-contiguous element
-// storage, but it will affect even a std::vector, because when you copy a
-// std::vector<std::string> the underlying data storage position for the char
-// arrays of the copy are likely to have high locality
-template <class Container>
-std::pair<Container, Container> genCacheUnfriendlyData(size_t size1, size_t size2, OverlapPosition pos) {
-  using ValueType = typename Container::value_type;
-  auto move_into  = [](auto first, auto last) {
-    Container out;
-    std::move(first, last, std::inserter(out, out.begin()));
-    return out;
-  };
-  const auto src_size        = pos == OverlapPosition::None ? size1 + size2 : std::max(size1, size2);
-  std::vector<ValueType> src = getVectorOfRandom<ValueType>(src_size);
-
-  if (pos == OverlapPosition::None) {
-    std::sort(src.begin(), src.end());
-    return std::make_pair(move_into(src.begin(), src.begin() + size1), move_into(src.begin() + size1, src.end()));
-  }
-
-  // All other overlap types will have to copy some part of the data, but if
-  // we copy after sorting it will likely have high locality, so we sort
-  // each copy separately
-  auto copy = src;
-  std::sort(src.begin(), src.end());
-  std::sort(copy.begin(), copy.end());
-
-  switch (pos) {
-  case OverlapPosition::None:
-    // we like -Wswitch :)
-    break;
-
-  case OverlapPosition::Front:
-    return std::make_pair(move_into(src.begin(), src.begin() + size1), move_into(copy.begin(), copy.begin() + size2));
-
-  case OverlapPosition::Interlaced:
-    const auto stride1 = size1 < size2 ? size2 / size1 : 1;
-    const auto stride2 = size2 < size1 ? size1 / size2 : 1;
-    return std::make_pair(move_into(StridedFwdIt(src.begin(), stride1), StridedFwdIt(src.end(), stride1)),
-                          move_into(StridedFwdIt(copy.begin(), stride2), StridedFwdIt(copy.end(), stride2)));
-  }
-  std::abort(); // would be std::unreachable() if it could
-  return std::pair<Container, Container>();
-}
-
-template <class ValueType, class Container, class Overlap>
-struct SetIntersection {
-  using ContainerType = typename Container::template type<Value<ValueType>>;
-  size_t size1_;
-  size_t size2_;
-
-  SetIntersection(size_t size1, size_t size2) : size1_(size1), size2_(size2) {}
-
-  bool skip() const noexcept {
-    // let's save some time and skip simmetrical runs
-    return size1_ < size2_;
-  }
-
-  void run(benchmark::State& state) const {
-    auto input = genCacheUnfriendlyData<ContainerType>(size1_, size2_, Overlap());
-    std::vector<Value<ValueType>> out(std::min(size1_, size2_));
-
-    const auto BATCH_SIZE = std::max(size_t{512}, (2 * TestSetElements) / (size1_ + size2_));
-    for (const auto& _ : state) {
-      while (state.KeepRunningBatch(BATCH_SIZE)) {
-        for (unsigned i = 0; i < BATCH_SIZE; ++i) {
-          const auto& [c1, c2] = input;
-          auto res             = std::set_intersection(c1.begin(), c1.end(), c2.begin(), c2.end(), out.begin());
-          benchmark::DoNotOptimize(res);
-        }
-      }
-    }
-  }
-
-  std::string name() const {
-    return std::string("SetIntersection") + Overlap::name() + '_' + Container::Name + ValueType::name() + '_' +
-           std::to_string(size1_) + '_' + std::to_string(size2_);
-  }
-};
-
-} // namespace
-
-int main(int argc, char** argv) { /**/
-  benchmark::Initialize(&argc, argv);
-  if (benchmark::ReportUnrecognizedArguments(argc, argv))
-    return 1;
-
-  makeCartesianProductBenchmark<SetIntersection, AllValueTypes, AllContainerTypes, AllOverlapPositions>(
-      Quantities, Quantities);
-  benchmark::RunSpecifiedBenchmarks();
-  return 0;
-}
diff --git a/libcxx/test/benchmarks/algorithms/sorting/set_intersection.bench.cpp b/libcxx/test/benchmarks/algorithms/sorting/set_intersection.bench.cpp
new file mode 100644
index 0000000000000..4c5ee2c13b735
--- /dev/null
+++ b/libcxx/test/benchmarks/algorithms/sorting/set_intersection.bench.cpp
@@ -0,0 +1,206 @@
+//===----------------------------------------------------------------------===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+
+// UNSUPPORTED: c++03, c++11, c++14, c++17
+
+#include <algorithm>
+#include <cstddef>
+#include <deque>
+#include <iterator>
+#include <set>
+#include <string>
+#include <type_traits>
+#include <utility>
+#include <vector>
+
+#include "benchmark/benchmark.h"
+#include "../../GenerateInput.h"
+
+// Each benchmarking scenario produces the two sorted ranges to intersect, with `n` the size of the
+// larger of the two. The size of the smaller range is derived from `n`, and the ratio between the
+// two appears in the benchmark's name. That ratio is used to exercise implementation choices that
+// make a difference when the two ranges have very different sizes.
+
+// Generate `n` random values and return two independently allocated sorted copies of them.
+//
+// The copy is made before sorting, so that the two copies don't end up with their out-of-line
+// storage laid out in the same order.
+template <class T>
+std::pair<std::vector<T>, std::vector<T>> two_sorted_copies(std::size_t n) {
+  std::vector<T> a(n);
+  std::generate_n(a.begin(), n, [] { return Generate<T>::random(); });
+  std::vector<T> b = a;
+  std::sort(a.begin(), a.end());
+  std::sort(b.begin(), b.end());
+  return std::make_pair(std::move(a), std::move(b));
+}
+
+// Generate two ranges where the larger range holds `n` elements and the smaller one holds every `ratio`th
+// element of it, so the whole smaller range matches and the matches are spread evenly through the larger
+// one. Whether searching within the larger range beats scanning it depends on how far apart the matches
+// are, which is why this scenario is instantiated with several ratios. With a ratio of 1 the two ranges
+// are identical, which is the worst case for the number of elements written to the output.
+template <class T>
+std::pair<std::vector<T>, std::vector<T>> spread(std::size_t n, std::size_t ratio) {
+  auto [large, sampled] = two_sorted_copies<T>(n);
+  std::vector<T> small;
+  small.reserve(n / ratio + 1);
+  for (std::size_t i = 0; i < n; i += ratio)
+    small.push_back(std::move(sampled[i]));
+  return std::make_pair(std::move(large), std::move(small));
+}
+
+int main(int argc, char** argv) {
+  auto bm = []<class Container>(std::string name, auto generate) {
+    return benchmark::RegisterBenchmark(name, [generate](auto& st) {
+      using ValueType        = typename Container::value_type;
+      std::size_t const size = st.range(0);
+
+      // Generate the values, then shuffle them so we don't insert into the containers in sorted order.
+      // Otherwise, node-based containers end up with an artificially good memory layout.
+      auto [values1, values2] = generate(std::type_identity<ValueType>{}, size);
+      std::shuffle(values1.begin(), values1.end(), getRandomEngine());
+      std::shuffle(values2.begin(), values2.end(), getRandomEngine());
+      Container c1(values1.begin(), values1.end());
+      Container c2(values2.begin(), values2.end());
+
+      // For containers that are not already sorted by construction, sort since that's a pre-requisite
+      // for std::set_intersection.
+      static constexpr bool is_ordered_container = requires(Container c, ValueType v) { c.lower_bound(v); };
+      if constexpr (!is_ordered_container) {
+        std::sort(c1.begin(), c1.end());
+        std::sort(c2.begin(), c2.end());
+      }
+
+      std::vector<ValueType> out(std::min(c1.size(), c2.size()));
+
+      for (auto _ : st) {
+        benchmark::DoNotOptimize(c1);
+        benchmark::DoNotOptimize(c2);
+        benchmark::DoNotOptimize(out);
+        auto result = std::set_intersection(c1.begin(), c1.end(), c2.begin(), c2.end(), out.begin());
+        benchmark::DoNotOptimize(result);
+      }
+    });
+  };
+
+  // Every scenario is registered with 32, which is small enough that constant factors dominate, 8192,
+  // which fits in cache, and 1 << 18, which does not. The std::string rows stop at 8192, because those
+  // elements are large and expensive to compare and to copy, so beyond that the benchmark spends most
+  // of its time in its own setup.
+
+  {
+    // The two ranges have no element in common, and every element of the first range compares less
+    // than every element of the second one. An implementation that searches ahead can dismiss the
+    // whole first range almost immediately.
+    auto disjoint = []<class T>(std::type_identity<T>, std::size_t n) {
+      std::vector<T> values(2 * n);
+      std::generate_n(values.begin(), 2 * n, [] { return Generate<T>::random(); });
+      std::sort(values.begin(), values.end());
+      std::vector<T> first(std::make_move_iterator(values.begin()), std::make_move_iterator(values.begin() + n));
+      std::vector<T> second(std::make_move_iterator(values.begin() + n), std::make_move_iterator(values.end()));
+      return std::make_pair(std::move(first), std::move(second));
+    };
+
+    // clang-format off
+    bm.operator()<std::vector<int>>("std::set_intersection(vector<int>) (disjoint)", disjoint)->Arg(32)->Arg(8192)->Arg(1 << 18);
+    bm.operator()<std::deque<int>>("std::set_intersection(deque<int>) (disjoint)", disjoint)->Arg(32)->Arg(8192)->Arg(1 << 18);
+    bm.operator()<std::set<int>>("std::set_intersection(set<int>) (disjoint)", disjoint)->Arg(32)->Arg(8192)->Arg(1 << 18);
+    bm.operator()<std::vector<std::string>>("std::set_intersection(vector<std::string>) (disjoint)", disjoint)->Arg(32)->Arg(8192);
+    bm.operator()<std::set<std::string>>("std::set_intersection(set<std::string>) (disjoint)", disjoint)->Arg(32)->Arg(8192);
+    // clang-format on
+  }
+
+  {
+    // The two ranges have no element in common and their elements strictly alternate:
+    // a[0] < b[0] < a[1] < b[1] < ... An implementation that searches ahead in one range
+    // can never advance by more than a single element.
+    auto interleaved = []<class T>(std::type_identity<T>, std::size_t n) {
+      std::vector<T> values(2 * n);
+      std::generate_n(values.begin(), 2 * n, [] { return Generate<T>::random(); });
+      std::sort(values.begin(), values.end());
+      std::vector<T> first;
+      std::vector<T> second;
+      first.reserve(n);
+      second.reserve(n);
+      for (std::size_t i = 0; i != values.size(); ++i)
+        (i % 2 == 0 ? first : second).push_back(std::move(values[i]));
+      return std::make_pair(std::move(first), std::move(second));
+    };
+
+    // clang-format off
+    bm.operator()<std::vector<int>>("std::set_intersection(vector<int>) (interleaved)", interleaved)->Arg(32)->Arg(8192)->Arg(1 << 18);
+    bm.operator()<std::deque<int>>("std::set_intersection(deque<int>) (interleaved)", interleaved)->Arg(32)->Arg(8192)->Arg(1 << 18);
+    bm.operator()<std::set<int>>("std::set_intersection(set<int>) (interleaved)", interleaved)->Arg(32)->Arg(8192)->Arg(1 << 18);
+    bm.operator()<std::vector<std::string>>("std::set_intersection(vector<std::string>) (interleaved)", interleaved)->Arg(32)->Arg(8192);
+    bm.operator()<std::set<std::string>>("std::set_intersection(set<std::string>) (interleaved)", interleaved)->Arg(32)->Arg(8192);
+    // clang-format on
+  }
+
+  {
+    auto identical = []<class T>(std::type_identity<T>, std::size_t n) { return spread<T>(n, 1); };
+
+    // clang-format off
+    bm.operator()<std::vector<int>>("std::set_intersection(vector<int>) (identical)", identical)->Arg(32)->Arg(8192)->Arg(1 << 18);
+    bm.operator()<std::deque<int>>("std::set_intersection(deque<int>) (identical)", identical)->Arg(32)->Arg(8192)->Arg(1 << 18);
+    bm.operator()<std::set<int>>("std::set_intersection(set<int>) (identical)", identical)->Arg(32)->Arg(8192)->Arg(1 << 18);
+    bm.operator()<std::vector<std::string>>("std::set_intersection(vector<std::string>) (identical)", identical)->Arg(32)->Arg(8192);
+    bm.operator()<std::set<std::string>>("std::set_intersection(set<std::string>) (identical)", identical)->Arg(32)->Arg(8192);
+    // clang-format on
+  }
+
+  {
+    auto spread_1_8 = []<class T>(std::type_identity<T>, std::size_t n) { return spread<T>(n, 8); };
+
+    // clang-format off
+    bm.operator()<std::vector<int>>("std::set_intersection(vector<int>) (spread 1:8)", spread_1_8)->Arg(32)->Arg(8192)->Arg(1 << 18);
+    bm.operator()<std::deque<int>>("std::set_intersection(deque<int>) (spread 1:8)", spread_1_8)->Arg(32)->Arg(8192)->Arg(1 << 18);
+    bm.operator()<std::set<int>>("std::set_intersection(set<int>) (spread 1:8)", spread_1_8)->Arg(32)->Arg(8192)->Arg(1 << 18);
+    bm.operator()<std::vector<std::string>>("std::set_intersection(vector<std::string>) (spread 1:8)", spread_1_8)->Arg(32)->Arg(8192);
+    bm.operator()<std::set<std::string>>("std::set_intersection(set<std::string>) (spread 1:8)", spread_1_8)->Arg(32)->Arg(8192);
+    // clang-format on
+  }
+
+  {
+    auto spread_1_1024 = []<class T>(std::type_identity<T>, std::size_t n) { return spread<T>(n, 1024); };
+
+    // clang-format off
+    bm.operator()<std::vector<int>>("std::set_intersection(vector<int>) (spread 1:1024)", spread_1_1024)->Arg(8192)->Arg(1 << 18);
+    bm.operator()<std::deque<int>>("std::set_intersection(deque<int>) (spread 1:1024)", spread_1_1024)->Arg(8192)->Arg(1 << 18);
+    bm.operator()<std::set<int>>("std::set_intersection(set<int>) (spread 1:1024)", spread_1_1024)->Arg(8192)->Arg(1 << 18);
+    bm.operator()<std::vector<std::string>>("std::set_intersection(vector<std::string>) (spread 1:1024)", spread_1_1024)->Arg(8192);
+    bm.operator()<std::set<std::string>>("std::set_intersection(set<std::string>) (spread 1:1024)", spread_1_1024)->Arg(8192);
+    // clang-format on
+  }
+
+  {
+    // The larger range holds `n` elements and the smaller one is its first `n / ratio` elements, so all the
+    // matches sit at the very front. An implementation stops as soon as it has exhausted the smaller range,
+    // so the cost should be proportional to the size of the smaller range, regardless of the contents of the
+    // larger range.
+    auto prefix = []<class T>(std::type_identity<T>, std::size_t n, std::size_t ratio) {
+      auto [large, sampled] = two_sorted_copies<T>(n);
+      sampled.erase(sampled.begin() + std::max<std::size_t>(1, n / ratio), sampled.end());
+      return std::make_pair(std::move(large), std::move(sampled));
+    };
+    auto prefix_1_1024 = [prefix](auto type, std::size_t n) { return prefix(type, n, 1024); };
+
+    // clang-format off
+    bm.operator()<std::vector<int>>("std::set_intersection(vector<int>) (prefix 1:1024)", prefix_1_1024)->Arg(8192)->Arg(1 << 18);
+    bm.operator()<std::deque<int>>("std::set_intersection(deque<int>) (prefix 1:1024)", prefix_1_1024)->Arg(8192)->Arg(1 << 18);
+    bm.operator()<std::set<int>>("std::set_intersection(set<int>) (prefix 1:1024)", prefix_1_1024)->Arg(8192)->Arg(1 << 18);
+    bm.operator()<std::vector<std::string>>("std::set_intersection(vector<std::string>) (prefix 1:1024)", prefix_1_1024)->Arg(8192);
+    bm.operator()<std::set<std::string>>("std::set_intersection(set<std::string>) (prefix 1:1024)", prefix_1_1024)->Arg(8192);
+    // clang-format on
+  }
+
+  benchmark::Initialize(&argc, argv);
+  benchmark::RunSpecifiedBenchmarks();
+  benchmark::Shutdown();
+  return 0;
+}

>From ead6ec2217b12690b3f9733acfb040e5895911d3 Mon Sep 17 00:00:00 2001
From: Louis Dionne <ldionne.2 at gmail.com>
Date: Thu, 6 Aug 2026 14:51:25 -0400
Subject: [PATCH 2/2] Tweaks to reduce noise

---
 .../algorithms/sorting/set_intersection.bench.cpp | 15 +++++++++------
 1 file changed, 9 insertions(+), 6 deletions(-)

diff --git a/libcxx/test/benchmarks/algorithms/sorting/set_intersection.bench.cpp b/libcxx/test/benchmarks/algorithms/sorting/set_intersection.bench.cpp
index 4c5ee2c13b735..a6d87fe83e403 100644
--- a/libcxx/test/benchmarks/algorithms/sorting/set_intersection.bench.cpp
+++ b/libcxx/test/benchmarks/algorithms/sorting/set_intersection.bench.cpp
@@ -92,7 +92,8 @@ int main(int argc, char** argv) {
   // Every scenario is registered with 32, which is small enough that constant factors dominate, 8192,
   // which fits in cache, and 1 << 18, which does not. The std::string rows stop at 8192, because those
   // elements are large and expensive to compare and to copy, so beyond that the benchmark spends most
-  // of its time in its own setup.
+  // of its time in its own setup. Most benchmarks on std::set stop at 8192, because they are too noisy
+  // above that.
 
   {
     // The two ranges have no element in common, and every element of the first range compares less
@@ -110,7 +111,7 @@ int main(int argc, char** argv) {
     // clang-format off
     bm.operator()<std::vector<int>>("std::set_intersection(vector<int>) (disjoint)", disjoint)->Arg(32)->Arg(8192)->Arg(1 << 18);
     bm.operator()<std::deque<int>>("std::set_intersection(deque<int>) (disjoint)", disjoint)->Arg(32)->Arg(8192)->Arg(1 << 18);
-    bm.operator()<std::set<int>>("std::set_intersection(set<int>) (disjoint)", disjoint)->Arg(32)->Arg(8192)->Arg(1 << 18);
+    bm.operator()<std::set<int>>("std::set_intersection(set<int>) (disjoint)", disjoint)->Arg(32)->Arg(8192);
     bm.operator()<std::vector<std::string>>("std::set_intersection(vector<std::string>) (disjoint)", disjoint)->Arg(32)->Arg(8192);
     bm.operator()<std::set<std::string>>("std::set_intersection(set<std::string>) (disjoint)", disjoint)->Arg(32)->Arg(8192);
     // clang-format on
@@ -136,7 +137,7 @@ int main(int argc, char** argv) {
     // clang-format off
     bm.operator()<std::vector<int>>("std::set_intersection(vector<int>) (interleaved)", interleaved)->Arg(32)->Arg(8192)->Arg(1 << 18);
     bm.operator()<std::deque<int>>("std::set_intersection(deque<int>) (interleaved)", interleaved)->Arg(32)->Arg(8192)->Arg(1 << 18);
-    bm.operator()<std::set<int>>("std::set_intersection(set<int>) (interleaved)", interleaved)->Arg(32)->Arg(8192)->Arg(1 << 18);
+    bm.operator()<std::set<int>>("std::set_intersection(set<int>) (interleaved)", interleaved)->Arg(32)->Arg(8192);
     bm.operator()<std::vector<std::string>>("std::set_intersection(vector<std::string>) (interleaved)", interleaved)->Arg(32)->Arg(8192);
     bm.operator()<std::set<std::string>>("std::set_intersection(set<std::string>) (interleaved)", interleaved)->Arg(32)->Arg(8192);
     // clang-format on
@@ -148,7 +149,7 @@ int main(int argc, char** argv) {
     // clang-format off
     bm.operator()<std::vector<int>>("std::set_intersection(vector<int>) (identical)", identical)->Arg(32)->Arg(8192)->Arg(1 << 18);
     bm.operator()<std::deque<int>>("std::set_intersection(deque<int>) (identical)", identical)->Arg(32)->Arg(8192)->Arg(1 << 18);
-    bm.operator()<std::set<int>>("std::set_intersection(set<int>) (identical)", identical)->Arg(32)->Arg(8192)->Arg(1 << 18);
+    bm.operator()<std::set<int>>("std::set_intersection(set<int>) (identical)", identical)->Arg(32)->Arg(8192);
     bm.operator()<std::vector<std::string>>("std::set_intersection(vector<std::string>) (identical)", identical)->Arg(32)->Arg(8192);
     bm.operator()<std::set<std::string>>("std::set_intersection(set<std::string>) (identical)", identical)->Arg(32)->Arg(8192);
     // clang-format on
@@ -160,7 +161,7 @@ int main(int argc, char** argv) {
     // clang-format off
     bm.operator()<std::vector<int>>("std::set_intersection(vector<int>) (spread 1:8)", spread_1_8)->Arg(32)->Arg(8192)->Arg(1 << 18);
     bm.operator()<std::deque<int>>("std::set_intersection(deque<int>) (spread 1:8)", spread_1_8)->Arg(32)->Arg(8192)->Arg(1 << 18);
-    bm.operator()<std::set<int>>("std::set_intersection(set<int>) (spread 1:8)", spread_1_8)->Arg(32)->Arg(8192)->Arg(1 << 18);
+    bm.operator()<std::set<int>>("std::set_intersection(set<int>) (spread 1:8)", spread_1_8)->Arg(32)->Arg(8192);
     bm.operator()<std::vector<std::string>>("std::set_intersection(vector<std::string>) (spread 1:8)", spread_1_8)->Arg(32)->Arg(8192);
     bm.operator()<std::set<std::string>>("std::set_intersection(set<std::string>) (spread 1:8)", spread_1_8)->Arg(32)->Arg(8192);
     // clang-format on
@@ -172,7 +173,7 @@ int main(int argc, char** argv) {
     // clang-format off
     bm.operator()<std::vector<int>>("std::set_intersection(vector<int>) (spread 1:1024)", spread_1_1024)->Arg(8192)->Arg(1 << 18);
     bm.operator()<std::deque<int>>("std::set_intersection(deque<int>) (spread 1:1024)", spread_1_1024)->Arg(8192)->Arg(1 << 18);
-    bm.operator()<std::set<int>>("std::set_intersection(set<int>) (spread 1:1024)", spread_1_1024)->Arg(8192)->Arg(1 << 18);
+    bm.operator()<std::set<int>>("std::set_intersection(set<int>) (spread 1:1024)", spread_1_1024)->Arg(8192);
     bm.operator()<std::vector<std::string>>("std::set_intersection(vector<std::string>) (spread 1:1024)", spread_1_1024)->Arg(8192);
     bm.operator()<std::set<std::string>>("std::set_intersection(set<std::string>) (spread 1:1024)", spread_1_1024)->Arg(8192);
     // clang-format on
@@ -183,6 +184,8 @@ int main(int argc, char** argv) {
     // matches sit at the very front. An implementation stops as soon as it has exhausted the smaller range,
     // so the cost should be proportional to the size of the smaller range, regardless of the contents of the
     // larger range.
+    //
+    // Here we bother testing std::set with a large range since we don't actually go through the full set.
     auto prefix = []<class T>(std::type_identity<T>, std::size_t n, std::size_t ratio) {
       auto [large, sampled] = two_sorted_copies<T>(n);
       sampled.erase(sampled.begin() + std::max<std::size_t>(1, n / ratio), sampled.end());



More information about the libcxx-commits mailing list