Skip to content

[libc++] Add benchmarks for partitioning algorithms #127324

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

Merged
merged 5 commits into from
Mar 24, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions libcxx/include/module.modulemap
Original file line number Diff line number Diff line change
Expand Up @@ -687,6 +687,7 @@ module std [system] {
}
module ranges_partition {
header "__algorithm/ranges_partition.h"
export std.ranges.subrange // return type
}
module ranges_pop_heap {
header "__algorithm/ranges_pop_heap.h"
Expand Down Expand Up @@ -786,6 +787,7 @@ module std [system] {
}
module ranges_stable_partition {
header "__algorithm/ranges_stable_partition.h"
export std.ranges.subrange // return type
}
module ranges_stable_sort {
header "__algorithm/ranges_stable_sort.h"
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
//===----------------------------------------------------------------------===//
//
// 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 <cassert>
#include <cstddef>
#include <deque>
#include <iterator>
#include <list>
#include <string>
#include <vector>

#include "benchmark/benchmark.h"
#include "../../GenerateInput.h"

auto compute_median(auto first, auto last) {
std::vector v(first, last);
auto middle = v.begin() + v.size() / 2;
std::nth_element(v.begin(), middle, v.end());
return *middle;
}

int main(int argc, char** argv) {
auto std_is_partitioned = [](auto first, auto last, auto pred) { return std::is_partitioned(first, last, pred); };

auto bm = []<class Container, bool Partitioned>(std::string name, auto is_partitioned) {
benchmark::RegisterBenchmark(
name,
[is_partitioned](auto& st) {
std::size_t const size = st.range(0);
using ValueType = typename Container::value_type;
Container c;
std::generate_n(std::back_inserter(c), size, [] { return Generate<ValueType>::random(); });

// Partition the container in two equally-sized halves, ensuring the median
// value appears in the left half. Note that the median value isn't located
// in the middle -- this isn't std::nth_element.
ValueType median = compute_median(c.begin(), c.end());
auto pred = [median](auto const& element) { return element <= median; };
std::partition(c.begin(), c.end(), pred);
assert(std::is_partitioned(c.begin(), c.end(), pred));

if constexpr (!Partitioned) {
// De-partition the container by swapping the element containing the median
// value with the last one.
auto median_it = std::find(c.begin(), c.end(), median);
auto last_it = std::next(c.begin(), c.size() - 1);
std::iter_swap(median_it, last_it);
assert(!std::is_partitioned(c.begin(), c.end(), pred));
}

for ([[maybe_unused]] auto _ : st) {
benchmark::DoNotOptimize(c);
auto result = is_partitioned(c.begin(), c.end(), pred);
benchmark::DoNotOptimize(result);
}
})
->Arg(32)
->Arg(50) // non power-of-two
->Arg(1024)
->Arg(8192);
};

// std::is_partitioned
bm.operator()<std::vector<int>, true>("std::is_partitioned(vector<int>) (partitioned)", std_is_partitioned);
bm.operator()<std::vector<int>, false>("std::is_partitioned(vector<int>) (unpartitioned)", std_is_partitioned);

bm.operator()<std::deque<int>, true>("std::is_partitioned(deque<int>) (partitioned)", std_is_partitioned);
bm.operator()<std::deque<int>, false>("std::is_partitioned(deque<int>) (unpartitioned)", std_is_partitioned);

bm.operator()<std::list<int>, true>("std::is_partitioned(list<int>) (partitioned)", std_is_partitioned);
bm.operator()<std::list<int>, false>("std::is_partitioned(list<int>) (unpartitioned)", std_is_partitioned);

// ranges::is_partitioned
bm.operator()<std::vector<int>, true>("rng::is_partitioned(vector<int>) (partitioned)", std::ranges::is_partitioned);
bm.operator()<std::vector<int>, false>(
"rng::is_partitioned(vector<int>) (unpartitioned)", std::ranges::is_partitioned);

bm.operator()<std::deque<int>, true>("rng::is_partitioned(deque<int>) (partitioned)", std::ranges::is_partitioned);
bm.operator()<std::deque<int>, false>("rng::is_partitioned(deque<int>) (unpartitioned)", std::ranges::is_partitioned);

bm.operator()<std::list<int>, true>("rng::is_partitioned(list<int>) (partitioned)", std::ranges::is_partitioned);
bm.operator()<std::list<int>, false>("rng::is_partitioned(list<int>) (unpartitioned)", std::ranges::is_partitioned);

benchmark::Initialize(&argc, argv);
benchmark::RunSpecifiedBenchmarks();
benchmark::Shutdown();
return 0;
}
133 changes: 133 additions & 0 deletions libcxx/test/benchmarks/algorithms/partitions/partition.bench.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
//===----------------------------------------------------------------------===//
//
// 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 <cassert>
#include <cstddef>
#include <deque>
#include <iterator>
#include <list>
#include <string>
#include <vector>

#include "benchmark/benchmark.h"
#include "../../GenerateInput.h"

auto compute_median(auto first, auto last) {
std::vector v(first, last);
auto middle = v.begin() + v.size() / 2;
std::nth_element(v.begin(), middle, v.end());
return *middle;
}

int main(int argc, char** argv) {
auto std_partition = [](auto first, auto last, auto pred) { return std::partition(first, last, pred); };

// Benchmark {std,ranges}::partition on a fully unpartitionned sequence, i.e. a lot of elements
// have to be moved around in order to partition the range.
{
auto bm = []<class Container>(std::string name, auto partition) {
benchmark::RegisterBenchmark(
name,
[partition](auto& st) {
std::size_t const size = st.range(0);
using ValueType = typename Container::value_type;
Container c;
std::generate_n(std::back_inserter(c), size, [] { return Generate<ValueType>::random(); });

ValueType median = compute_median(c.begin(), c.end());
auto pred1 = [median](auto const& element) { return element < median; };
auto pred2 = [median](auto const& element) { return element > median; };
bool toggle = false;

for ([[maybe_unused]] auto _ : st) {
benchmark::DoNotOptimize(c);
// By toggling the predicate, we have to move almost all elements in the sequence
// to restore the partition.
if (toggle) {
auto result = partition(c.begin(), c.end(), pred1);
benchmark::DoNotOptimize(result);
} else {
auto result = partition(c.begin(), c.end(), pred2);
benchmark::DoNotOptimize(result);
}
toggle = !toggle;
}
})
->Arg(32)
->Arg(50) // non power-of-two
->Arg(1024)
->Arg(8192);
};

// std::partition
bm.operator()<std::vector<int>>("std::partition(vector<int>) (dense)", std_partition);
bm.operator()<std::deque<int>>("std::partition(deque<int>) (dense)", std_partition);
bm.operator()<std::list<int>>("std::partition(list<int>) (dense)", std_partition);

// ranges::partition
bm.operator()<std::vector<int>>("rng::partition(vector<int>) (dense)", std::ranges::partition);
bm.operator()<std::deque<int>>("rng::partition(deque<int>) (dense)", std::ranges::partition);
bm.operator()<std::list<int>>("rng::partition(list<int>) (dense)", std::ranges::partition);
}

// Benchmark {std,ranges}::partition on a mostly partitioned sequence, i.e. only 10% of the elements
// have to be moved around in order to partition the range.
{
auto bm = []<class Container>(std::string name, auto partition) {
benchmark::RegisterBenchmark(
name,
[partition](auto& st) {
std::size_t const size = st.range(0);
using ValueType = typename Container::value_type;
Container c;
std::generate_n(std::back_inserter(c), size, [] { return Generate<ValueType>::random(); });
ValueType median = compute_median(c.begin(), c.end());
auto pred = [median](auto const& element) { return element < median; };
std::partition(c.begin(), c.end(), pred);

// Between iterations, we swap 5% of the elements to the left of the median with 5% of the elements
// to the right of the median. This ensures that the range is slightly unpartitioned.
auto median_it = std::partition_point(c.begin(), c.end(), pred);
auto low = std::next(c.begin(), std::distance(c.begin(), median_it) - (size / 20));
auto high = std::next(median_it, size / 20);
auto shuffle = [&] { std::swap_ranges(low, median_it, high); };
shuffle();
assert(!std::is_partitioned(c.begin(), c.end(), pred));

for ([[maybe_unused]] auto _ : st) {
benchmark::DoNotOptimize(c);
auto result = partition(c.begin(), c.end(), pred);
benchmark::DoNotOptimize(result);
shuffle();
}
})
->Arg(32)
->Arg(50) // non power-of-two
->Arg(1024)
->Arg(8192);
};

// std::partition
bm.operator()<std::vector<int>>("std::partition(vector<int>) (sparse)", std_partition);
bm.operator()<std::deque<int>>("std::partition(deque<int>) (sparse)", std_partition);
bm.operator()<std::list<int>>("std::partition(list<int>) (sparse)", std_partition);

// ranges::partition
bm.operator()<std::vector<int>>("rng::partition(vector<int>) (sparse)", std::ranges::partition);
bm.operator()<std::deque<int>>("rng::partition(deque<int>) (sparse)", std::ranges::partition);
bm.operator()<std::list<int>>("rng::partition(list<int>) (sparse)", std::ranges::partition);
}

benchmark::Initialize(&argc, argv);
benchmark::RunSpecifiedBenchmarks();
benchmark::Shutdown();
return 0;
}
Loading