From cf79e2c4c97023969913503cb5a4fdc0246d6c37 Mon Sep 17 00:00:00 2001 From: Louis Dionne Date: Thu, 6 Feb 2025 13:12:40 -0500 Subject: [PATCH 1/2] [libc++] Further refactor sequence container benchmarks This patch does not significantly change how the sequence container benchmarks are done, but it adopts the same style as the associative container benchmarks. However, this commit does adjust how we were benchmarking push_back, where we never really measured the overhead of the slow path of push_back (when we need to reallocate). --- .../containers/container_benchmarks.h | 609 ------------------ .../containers/{ => sequence}/deque.bench.cpp | 6 +- .../containers/{ => sequence}/list.bench.cpp | 6 +- .../sequence/sequence_container_benchmarks.h | 456 +++++++++++++ .../{ => sequence}/vector.bench.cpp | 6 +- 5 files changed, 465 insertions(+), 618 deletions(-) delete mode 100644 libcxx/test/benchmarks/containers/container_benchmarks.h rename libcxx/test/benchmarks/containers/{ => sequence}/deque.bench.cpp (73%) rename libcxx/test/benchmarks/containers/{ => sequence}/list.bench.cpp (73%) create mode 100644 libcxx/test/benchmarks/containers/sequence/sequence_container_benchmarks.h rename libcxx/test/benchmarks/containers/{ => sequence}/vector.bench.cpp (73%) diff --git a/libcxx/test/benchmarks/containers/container_benchmarks.h b/libcxx/test/benchmarks/containers/container_benchmarks.h deleted file mode 100644 index e24bd767177e8..0000000000000 --- a/libcxx/test/benchmarks/containers/container_benchmarks.h +++ /dev/null @@ -1,609 +0,0 @@ -// -*- C++ -*- -//===----------------------------------------------------------------------===// -// -// 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 TEST_BENCHMARKS_CONTAINERS_CONTAINER_BENCHMARKS_H -#define TEST_BENCHMARKS_CONTAINERS_CONTAINER_BENCHMARKS_H - -#include -#include -#include -#include // for std::from_range -#include -#include -#include - -#include "benchmark/benchmark.h" -#include "test_iterators.h" -#include "test_macros.h" -#include "../GenerateInput.h" - -namespace ContainerBenchmarks { - -template -void DoNotOptimizeData(Container& c) { - if constexpr (requires { c.data(); }) { - benchmark::DoNotOptimize(c.data()); - } else { - benchmark::DoNotOptimize(&c); - } -} - -// -// Sequence container operations -// -template -void BM_ctor_size(benchmark::State& st) { - auto size = st.range(0); - - for (auto _ : st) { - Container c(size); // we assume the destructor doesn't dominate the benchmark - DoNotOptimizeData(c); - } -} - -template -void BM_ctor_size_value(benchmark::State& st, Generator gen) { - using ValueType = typename Container::value_type; - const auto size = st.range(0); - ValueType value = gen(); - benchmark::DoNotOptimize(value); - - for (auto _ : st) { - Container c(size, value); // we assume the destructor doesn't dominate the benchmark - DoNotOptimizeData(c); - } -} - -template -void BM_ctor_iter_iter(benchmark::State& st, Generator gen) { - using ValueType = typename Container::value_type; - const auto size = st.range(0); - std::vector in; - std::generate_n(std::back_inserter(in), size, gen); - const auto begin = in.begin(); - const auto end = in.end(); - benchmark::DoNotOptimize(in); - - for (auto _ : st) { - Container c(begin, end); // we assume the destructor doesn't dominate the benchmark - DoNotOptimizeData(c); - } -} - -#if TEST_STD_VER >= 23 -template -void BM_ctor_from_range(benchmark::State& st, Generator gen) { - using ValueType = typename Container::value_type; - const auto size = st.range(0); - std::vector in; - std::generate_n(std::back_inserter(in), size, gen); - benchmark::DoNotOptimize(in); - - for (auto _ : st) { - Container c(std::from_range, in); // we assume the destructor doesn't dominate the benchmark - DoNotOptimizeData(c); - } -} -#endif - -template -void BM_ctor_copy(benchmark::State& st, Generator gen) { - auto size = st.range(0); - Container in; - std::generate_n(std::back_inserter(in), size, gen); - DoNotOptimizeData(in); - - for (auto _ : st) { - Container c(in); // we assume the destructor doesn't dominate the benchmark - DoNotOptimizeData(c); - DoNotOptimizeData(in); - } -} - -template -void BM_assignment(benchmark::State& st, Generator gen) { - auto size = st.range(0); - Container in1, in2; - std::generate_n(std::back_inserter(in1), size, gen); - std::generate_n(std::back_inserter(in2), size, gen); - DoNotOptimizeData(in1); - DoNotOptimizeData(in2); - - // Assign from one of two containers in succession to avoid - // hitting a self-assignment corner-case - Container c(in1); - bool toggle = false; - for (auto _ : st) { - c = toggle ? in1 : in2; - toggle = !toggle; - DoNotOptimizeData(c); - DoNotOptimizeData(in1); - DoNotOptimizeData(in2); - } -} - -// Benchmark Container::assign(input-iter, input-iter) when the container already contains -// the same number of elements that we're assigning. The intent is to check whether the -// implementation basically creates a new container from scratch or manages to reuse the -// pre-existing storage. -template -void BM_assign_input_iter_full(benchmark::State& st, Generator gen) { - using ValueType = typename Container::value_type; - auto size = st.range(0); - std::vector in1, in2; - std::generate_n(std::back_inserter(in1), size, gen); - std::generate_n(std::back_inserter(in2), size, gen); - DoNotOptimizeData(in1); - DoNotOptimizeData(in2); - - Container c(in1.begin(), in1.end()); - bool toggle = false; - for (auto _ : st) { - std::vector& in = toggle ? in1 : in2; - auto first = in.data(); - auto last = in.data() + in.size(); - c.assign(cpp17_input_iterator(first), cpp17_input_iterator(last)); - toggle = !toggle; - DoNotOptimizeData(c); - } -} - -template -void BM_insert_begin(benchmark::State& st, Generator gen) { - using ValueType = typename Container::value_type; - const int size = st.range(0); - std::vector in; - std::generate_n(std::back_inserter(in), size, gen); - DoNotOptimizeData(in); - - Container c(in.begin(), in.end()); - DoNotOptimizeData(c); - - ValueType value = gen(); - benchmark::DoNotOptimize(value); - - for (auto _ : st) { - c.insert(c.begin(), value); - DoNotOptimizeData(c); - - c.erase(std::prev(c.end())); // avoid growing indefinitely - } -} - -template - requires std::random_access_iterator -void BM_insert_middle(benchmark::State& st, Generator gen) { - using ValueType = typename Container::value_type; - const int size = st.range(0); - std::vector in; - std::generate_n(std::back_inserter(in), size, gen); - DoNotOptimizeData(in); - - Container c(in.begin(), in.end()); - DoNotOptimizeData(c); - - ValueType value = gen(); - benchmark::DoNotOptimize(value); - - for (auto _ : st) { - auto mid = c.begin() + (size / 2); // requires random-access iterators in order to make sense - c.insert(mid, value); - DoNotOptimizeData(c); - - c.erase(c.end() - 1); // avoid growing indefinitely - } -} - -// Insert at the start of a vector in a scenario where the vector already -// has enough capacity to hold all the elements we are inserting. -template -void BM_insert_begin_input_iter_with_reserve_no_realloc(benchmark::State& st, Generator gen) { - using ValueType = typename Container::value_type; - const int size = st.range(0); - std::vector in; - std::generate_n(std::back_inserter(in), size, gen); - DoNotOptimizeData(in); - auto first = in.data(); - auto last = in.data() + in.size(); - - const int small = 100; // arbitrary - Container c; - c.reserve(size + small); // ensure no reallocation - std::generate_n(std::back_inserter(c), small, gen); - - for (auto _ : st) { - c.insert(c.begin(), cpp17_input_iterator(first), cpp17_input_iterator(last)); - DoNotOptimizeData(c); - - st.PauseTiming(); - c.erase(c.begin() + small, c.end()); // avoid growing indefinitely - st.ResumeTiming(); - } -} - -// Insert at the start of a vector in a scenario where the vector already -// has almost enough capacity to hold all the elements we are inserting, -// but does need to reallocate. -template -void BM_insert_begin_input_iter_with_reserve_almost_no_realloc(benchmark::State& st, Generator gen) { - using ValueType = typename Container::value_type; - const int size = st.range(0); - std::vector in; - std::generate_n(std::back_inserter(in), size, gen); - DoNotOptimizeData(in); - auto first = in.data(); - auto last = in.data() + in.size(); - - const int overflow = size / 10; // 10% of elements won't fit in the vector when we insert - Container c; - for (auto _ : st) { - st.PauseTiming(); - c = Container(); - c.reserve(size); - std::generate_n(std::back_inserter(c), overflow, gen); - st.ResumeTiming(); - - c.insert(c.begin(), cpp17_input_iterator(first), cpp17_input_iterator(last)); - DoNotOptimizeData(c); - } -} - -// Insert at the start of a vector in a scenario where the vector can fit a few -// more elements, but needs to reallocate almost immediately to fit the remaining -// elements. -template -void BM_insert_begin_input_iter_with_reserve_near_full(benchmark::State& st, Generator gen) { - using ValueType = typename Container::value_type; - const int size = st.range(0); - std::vector in; - std::generate_n(std::back_inserter(in), size, gen); - DoNotOptimizeData(in); - auto first = in.data(); - auto last = in.data() + in.size(); - - const int overflow = 9 * (size / 10); // 90% of elements won't fit in the vector when we insert - Container c; - for (auto _ : st) { - st.PauseTiming(); - c = Container(); - c.reserve(size); - std::generate_n(std::back_inserter(c), overflow, gen); - st.ResumeTiming(); - - c.insert(c.begin(), cpp17_input_iterator(first), cpp17_input_iterator(last)); - DoNotOptimizeData(c); - } -} - -template -void BM_erase_begin(benchmark::State& st, Generator gen) { - using ValueType = typename Container::value_type; - const int size = st.range(0); - std::vector in; - std::generate_n(std::back_inserter(in), size, gen); - DoNotOptimizeData(in); - - Container c(in.begin(), in.end()); - DoNotOptimizeData(c); - - ValueType value = gen(); - benchmark::DoNotOptimize(value); - - for (auto _ : st) { - c.erase(c.begin()); - DoNotOptimizeData(c); - - c.insert(c.end(), value); // re-insert an element at the end to avoid needing a new container - } -} - -template - requires std::random_access_iterator -void BM_erase_middle(benchmark::State& st, Generator gen) { - using ValueType = typename Container::value_type; - const int size = st.range(0); - std::vector in; - std::generate_n(std::back_inserter(in), size, gen); - DoNotOptimizeData(in); - - Container c(in.begin(), in.end()); - DoNotOptimizeData(c); - - ValueType value = gen(); - benchmark::DoNotOptimize(value); - - for (auto _ : st) { - auto mid = c.begin() + (size / 2); - c.erase(mid); - DoNotOptimizeData(c); - - c.insert(c.end(), value); // re-insert an element at the end to avoid needing a new container - } -} - -template -void BM_push_back(benchmark::State& st, Generator gen) { - using ValueType = typename Container::value_type; - const int size = st.range(0); - std::vector in; - std::generate_n(std::back_inserter(in), size, gen); - DoNotOptimizeData(in); - - Container c; - DoNotOptimizeData(c); - while (st.KeepRunningBatch(size)) { - c.clear(); - for (int i = 0; i != size; ++i) { - c.push_back(in[i]); - } - DoNotOptimizeData(c); - } -} - -template -void BM_push_back_with_reserve(benchmark::State& st, Generator gen) { - using ValueType = typename Container::value_type; - const int size = st.range(0); - std::vector in; - std::generate_n(std::back_inserter(in), size, gen); - DoNotOptimizeData(in); - - Container c; - c.reserve(size); - DoNotOptimizeData(c); - while (st.KeepRunningBatch(size)) { - c.clear(); - for (int i = 0; i != size; ++i) { - c.push_back(in[i]); - } - DoNotOptimizeData(c); - } -} - -template -void sequence_container_benchmarks(std::string container) { - using ValueType = typename Container::value_type; - - using Generator = ValueType (*)(); - Generator cheap = [] { return Generate::cheap(); }; - Generator expensive = [] { return Generate::expensive(); }; - auto tostr = [&](Generator gen) { return gen == cheap ? " (cheap elements)" : " (expensive elements)"; }; - std::vector generators; - generators.push_back(cheap); - if constexpr (!std::is_integral_v) { - generators.push_back(expensive); - } - - // constructors - if constexpr (std::is_constructible_v) { - // not all containers provide this one - benchmark::RegisterBenchmark(container + "::ctor(size)", BM_ctor_size)->Arg(1024); - } - for (auto gen : generators) - benchmark::RegisterBenchmark(container + "::ctor(size, value_type)" + tostr(gen), [=](auto& st) { - BM_ctor_size_value(st, gen); - })->Arg(1024); - for (auto gen : generators) - benchmark::RegisterBenchmark(container + "::ctor(Iterator, Iterator)" + tostr(gen), [=](auto& st) { - BM_ctor_iter_iter(st, gen); - })->Arg(1024); -#if TEST_STD_VER >= 23 - for (auto gen : generators) - benchmark::RegisterBenchmark(container + "::ctor(Range)" + tostr(gen), [=](auto& st) { - BM_ctor_from_range(st, gen); - })->Arg(1024); -#endif - for (auto gen : generators) - benchmark::RegisterBenchmark(container + "::ctor(const&)" + tostr(gen), [=](auto& st) { - BM_ctor_copy(st, gen); - })->Arg(1024); - - // assignment - for (auto gen : generators) - benchmark::RegisterBenchmark(container + "::operator=(const&)" + tostr(gen), [=](auto& st) { - BM_assignment(st, gen); - })->Arg(1024); - for (auto gen : generators) - benchmark::RegisterBenchmark(container + "::assign(input-iter, input-iter) (full container)" + tostr(gen), - [=](auto& st) { BM_assign_input_iter_full(st, gen); }) - ->Arg(1024); - - // insert - for (auto gen : generators) - benchmark::RegisterBenchmark(container + "::insert(begin)" + tostr(gen), [=](auto& st) { - BM_insert_begin(st, gen); - })->Arg(1024); - if constexpr (std::random_access_iterator) { - for (auto gen : generators) - benchmark::RegisterBenchmark(container + "::insert(middle)" + tostr(gen), [=](auto& st) { - BM_insert_middle(st, gen); - })->Arg(1024); - } - if constexpr (requires(Container c) { c.reserve(0); }) { - for (auto gen : generators) - benchmark::RegisterBenchmark( - container + "::insert(begin, input-iter, input-iter) (no realloc)" + tostr(gen), - [=](auto& st) { BM_insert_begin_input_iter_with_reserve_no_realloc(st, gen); }) - ->Arg(1024); - for (auto gen : generators) - benchmark::RegisterBenchmark( - container + "::insert(begin, input-iter, input-iter) (half filled)" + tostr(gen), - [=](auto& st) { BM_insert_begin_input_iter_with_reserve_almost_no_realloc(st, gen); }) - ->Arg(1024); - for (auto gen : generators) - benchmark::RegisterBenchmark( - container + "::insert(begin, input-iter, input-iter) (near full)" + tostr(gen), - [=](auto& st) { BM_insert_begin_input_iter_with_reserve_near_full(st, gen); }) - ->Arg(1024); - } - - // erase - for (auto gen : generators) - benchmark::RegisterBenchmark(container + "::erase(begin)" + tostr(gen), [=](auto& st) { - BM_erase_begin(st, gen); - })->Arg(1024); - if constexpr (std::random_access_iterator) { - for (auto gen : generators) - benchmark::RegisterBenchmark(container + "::erase(middle)" + tostr(gen), [=](auto& st) { - BM_erase_middle(st, gen); - })->Arg(1024); - } - - // push_back (optional) - if constexpr (requires(Container c, ValueType v) { c.push_back(v); }) { - for (auto gen : generators) - benchmark::RegisterBenchmark(container + "::push_back()" + tostr(gen), [=](auto& st) { - BM_push_back(st, gen); - })->Arg(1024); - if constexpr (requires(Container c) { c.reserve(0); }) { - for (auto gen : generators) - benchmark::RegisterBenchmark(container + "::push_back() (with reserve)" + tostr(gen), [=](auto& st) { - BM_push_back_with_reserve(st, gen); - })->Arg(1024); - } - } -} - -// -// Misc operations -// -template -void BM_InsertValue(benchmark::State& st, Container c, GenInputs gen) { - auto in = gen(st.range(0)); - const auto end = in.end(); - while (st.KeepRunning()) { - c.clear(); - for (auto it = in.begin(); it != end; ++it) { - benchmark::DoNotOptimize(&(*c.insert(*it).first)); - } - benchmark::ClobberMemory(); - } -} - -template -void BM_InsertValueRehash(benchmark::State& st, Container c, GenInputs gen) { - auto in = gen(st.range(0)); - const auto end = in.end(); - while (st.KeepRunning()) { - c.clear(); - c.rehash(16); - for (auto it = in.begin(); it != end; ++it) { - benchmark::DoNotOptimize(&(*c.insert(*it).first)); - } - benchmark::ClobberMemory(); - } -} - -template -void BM_InsertDuplicate(benchmark::State& st, Container c, GenInputs gen) { - auto in = gen(st.range(0)); - const auto end = in.end(); - c.insert(in.begin(), in.end()); - benchmark::DoNotOptimize(c); - benchmark::DoNotOptimize(in); - while (st.KeepRunning()) { - for (auto it = in.begin(); it != end; ++it) { - benchmark::DoNotOptimize(&(*c.insert(*it).first)); - } - benchmark::ClobberMemory(); - } -} - -template -void BM_EmplaceDuplicate(benchmark::State& st, Container c, GenInputs gen) { - auto in = gen(st.range(0)); - const auto end = in.end(); - c.insert(in.begin(), in.end()); - benchmark::DoNotOptimize(c); - benchmark::DoNotOptimize(in); - while (st.KeepRunning()) { - for (auto it = in.begin(); it != end; ++it) { - benchmark::DoNotOptimize(&(*c.emplace(*it).first)); - } - benchmark::ClobberMemory(); - } -} - -template -void BM_Find(benchmark::State& st, Container c, GenInputs gen) { - auto in = gen(st.range(0)); - c.insert(in.begin(), in.end()); - benchmark::DoNotOptimize(&(*c.begin())); - const auto end = in.data() + in.size(); - while (st.KeepRunning()) { - for (auto it = in.data(); it != end; ++it) { - benchmark::DoNotOptimize(&(*c.find(*it))); - } - benchmark::ClobberMemory(); - } -} - -template -void BM_FindRehash(benchmark::State& st, Container c, GenInputs gen) { - c.rehash(8); - auto in = gen(st.range(0)); - c.insert(in.begin(), in.end()); - benchmark::DoNotOptimize(&(*c.begin())); - const auto end = in.data() + in.size(); - while (st.KeepRunning()) { - for (auto it = in.data(); it != end; ++it) { - benchmark::DoNotOptimize(&(*c.find(*it))); - } - benchmark::ClobberMemory(); - } -} - -template -void BM_Rehash(benchmark::State& st, Container c, GenInputs gen) { - auto in = gen(st.range(0)); - c.max_load_factor(3.0); - c.insert(in.begin(), in.end()); - benchmark::DoNotOptimize(c); - const auto bucket_count = c.bucket_count(); - while (st.KeepRunning()) { - c.rehash(bucket_count + 1); - c.rehash(bucket_count); - benchmark::ClobberMemory(); - } -} - -template -void BM_Compare_same_container(benchmark::State& st, Container, GenInputs gen) { - auto in = gen(st.range(0)); - Container c1(in.begin(), in.end()); - Container c2 = c1; - - benchmark::DoNotOptimize(&(*c1.begin())); - benchmark::DoNotOptimize(&(*c2.begin())); - while (st.KeepRunning()) { - bool res = c1 == c2; - benchmark::DoNotOptimize(&res); - benchmark::ClobberMemory(); - } -} - -template -void BM_Compare_different_containers(benchmark::State& st, Container, GenInputs gen) { - auto in1 = gen(st.range(0)); - auto in2 = gen(st.range(0)); - Container c1(in1.begin(), in1.end()); - Container c2(in2.begin(), in2.end()); - - benchmark::DoNotOptimize(&(*c1.begin())); - benchmark::DoNotOptimize(&(*c2.begin())); - while (st.KeepRunning()) { - bool res = c1 == c2; - benchmark::DoNotOptimize(&res); - benchmark::ClobberMemory(); - } -} - -} // namespace ContainerBenchmarks - -#endif // TEST_BENCHMARKS_CONTAINERS_CONTAINER_BENCHMARKS_H diff --git a/libcxx/test/benchmarks/containers/deque.bench.cpp b/libcxx/test/benchmarks/containers/sequence/deque.bench.cpp similarity index 73% rename from libcxx/test/benchmarks/containers/deque.bench.cpp rename to libcxx/test/benchmarks/containers/sequence/deque.bench.cpp index 6a650fa4dce2a..e37c9fef4ac23 100644 --- a/libcxx/test/benchmarks/containers/deque.bench.cpp +++ b/libcxx/test/benchmarks/containers/sequence/deque.bench.cpp @@ -11,12 +11,12 @@ #include #include -#include "container_benchmarks.h" +#include "sequence_container_benchmarks.h" #include "benchmark/benchmark.h" int main(int argc, char** argv) { - ContainerBenchmarks::sequence_container_benchmarks>("std::deque"); - ContainerBenchmarks::sequence_container_benchmarks>("std::deque"); + support::sequence_container_benchmarks>("std::deque"); + support::sequence_container_benchmarks>("std::deque"); benchmark::Initialize(&argc, argv); benchmark::RunSpecifiedBenchmarks(); diff --git a/libcxx/test/benchmarks/containers/list.bench.cpp b/libcxx/test/benchmarks/containers/sequence/list.bench.cpp similarity index 73% rename from libcxx/test/benchmarks/containers/list.bench.cpp rename to libcxx/test/benchmarks/containers/sequence/list.bench.cpp index 2212affa02ba4..e40aae6cf9fa5 100644 --- a/libcxx/test/benchmarks/containers/list.bench.cpp +++ b/libcxx/test/benchmarks/containers/sequence/list.bench.cpp @@ -11,12 +11,12 @@ #include #include -#include "container_benchmarks.h" +#include "sequence_container_benchmarks.h" #include "benchmark/benchmark.h" int main(int argc, char** argv) { - ContainerBenchmarks::sequence_container_benchmarks>("std::list"); - ContainerBenchmarks::sequence_container_benchmarks>("std::list"); + support::sequence_container_benchmarks>("std::list"); + support::sequence_container_benchmarks>("std::list"); benchmark::Initialize(&argc, argv); benchmark::RunSpecifiedBenchmarks(); diff --git a/libcxx/test/benchmarks/containers/sequence/sequence_container_benchmarks.h b/libcxx/test/benchmarks/containers/sequence/sequence_container_benchmarks.h new file mode 100644 index 0000000000000..d844d1d93a8ce --- /dev/null +++ b/libcxx/test/benchmarks/containers/sequence/sequence_container_benchmarks.h @@ -0,0 +1,456 @@ +// -*- C++ -*- +//===----------------------------------------------------------------------===// +// +// 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 TEST_BENCHMARKS_CONTAINERS_SEQUENCE_SEQUENCE_CONTAINER_BENCHMARKS_H +#define TEST_BENCHMARKS_CONTAINERS_SEQUENCE_SEQUENCE_CONTAINER_BENCHMARKS_H + +#include +#include +#include +#include +#include // for std::from_range +#include +#include +#include + +#include "benchmark/benchmark.h" +#include "test_iterators.h" +#include "test_macros.h" +#include "../../GenerateInput.h" + +namespace support { + +template +void DoNotOptimizeData(Container& c) { + if constexpr (requires { c.data(); }) { + benchmark::DoNotOptimize(c.data()); + } else { + benchmark::DoNotOptimize(&c); + } +} + +template +void sequence_container_benchmarks(std::string container) { + using ValueType = typename Container::value_type; + + using Generator = ValueType (*)(); + Generator cheap = [] { return Generate::cheap(); }; + Generator expensive = [] { return Generate::expensive(); }; + auto tostr = [&](Generator gen) -> std::string { + return gen == cheap ? " (cheap elements)" : " (expensive elements)"; + }; + std::vector generators; + generators.push_back(cheap); + if constexpr (!std::is_integral_v) { + generators.push_back(expensive); + } + + // Some of these benchmarks are structured to perform the operation being benchmarked + // a small number of times at each iteration, in order to offset the cost of + // PauseTiming() and ResumeTiming(). + static constexpr std::size_t BatchSize = 32; + + auto bench = [&](std::string operation, auto f) { + benchmark::RegisterBenchmark(container + "::" + operation, f)->Arg(32)->Arg(1024)->Arg(8192); + }; + + ///////////////////////// + // Constructors + ///////////////////////// + if constexpr (std::is_constructible_v) { + // not all containers provide this constructor + bench("ctor(size)", [](auto& st) { + auto const size = st.range(0); + + for ([[maybe_unused]] auto _ : st) { + Container c(size); // we assume the destructor doesn't dominate the benchmark + DoNotOptimizeData(c); + } + }); + } + + for (auto gen : generators) + bench("ctor(size, value_type)" + tostr(gen), [gen](auto& st) { + auto const size = st.range(0); + ValueType value = gen(); + benchmark::DoNotOptimize(value); + + for ([[maybe_unused]] auto _ : st) { + Container c(size, value); // we assume the destructor doesn't dominate the benchmark + DoNotOptimizeData(c); + } + }); + + for (auto gen : generators) + bench("ctor(Iterator, Iterator)" + tostr(gen), [gen](auto& st) { + auto const size = st.range(0); + std::vector in; + std::generate_n(std::back_inserter(in), size, gen); + const auto begin = in.begin(); + const auto end = in.end(); + benchmark::DoNotOptimize(in); + + for ([[maybe_unused]] auto _ : st) { + Container c(begin, end); // we assume the destructor doesn't dominate the benchmark + DoNotOptimizeData(c); + } + }); + +#if TEST_STD_VER >= 23 + for (auto gen : generators) + bench("ctor(Range)" + tostr(gen), [gen](auto& st) { + auto const size = st.range(0); + std::vector in; + std::generate_n(std::back_inserter(in), size, gen); + benchmark::DoNotOptimize(in); + + for ([[maybe_unused]] auto _ : st) { + Container c(std::from_range, in); // we assume the destructor doesn't dominate the benchmark + DoNotOptimizeData(c); + } + }); +#endif + + for (auto gen : generators) + bench("ctor(const&)" + tostr(gen), [gen](auto& st) { + auto const size = st.range(0); + Container in; + std::generate_n(std::back_inserter(in), size, gen); + DoNotOptimizeData(in); + + for ([[maybe_unused]] auto _ : st) { + Container c(in); // we assume the destructor doesn't dominate the benchmark + DoNotOptimizeData(c); + DoNotOptimizeData(in); + } + }); + + ///////////////////////// + // Assignment + ///////////////////////// + for (auto gen : generators) + bench("operator=(const&)" + tostr(gen), [gen](auto& st) { + auto const size = st.range(0); + Container in1, in2; + std::generate_n(std::back_inserter(in1), size, gen); + std::generate_n(std::back_inserter(in2), size, gen); + DoNotOptimizeData(in1); + DoNotOptimizeData(in2); + + // Assign from one of two containers in succession to avoid + // hitting a self-assignment corner-case + Container c(in1); + bool toggle = false; + for ([[maybe_unused]] auto _ : st) { + c = toggle ? in1 : in2; + toggle = !toggle; + DoNotOptimizeData(c); + DoNotOptimizeData(in1); + DoNotOptimizeData(in2); + } + }); + + // Benchmark Container::assign(input-iter, input-iter) when the container already contains + // the same number of elements that we're assigning. The intent is to check whether the + // implementation basically creates a new container from scratch or manages to reuse the + // pre-existing storage. + for (auto gen : generators) + bench("assign(input-iter, input-iter) (full container)" + tostr(gen), [gen](auto& st) { + auto const size = st.range(0); + std::vector in1, in2; + std::generate_n(std::back_inserter(in1), size, gen); + std::generate_n(std::back_inserter(in2), size, gen); + DoNotOptimizeData(in1); + DoNotOptimizeData(in2); + + Container c(in1.begin(), in1.end()); + bool toggle = false; + for ([[maybe_unused]] auto _ : st) { + std::vector& in = toggle ? in1 : in2; + auto first = in.data(); + auto last = in.data() + in.size(); + c.assign(cpp17_input_iterator(first), cpp17_input_iterator(last)); + toggle = !toggle; + DoNotOptimizeData(c); + } + }); + + ///////////////////////// + // Insertion + ///////////////////////// + for (auto gen : generators) + bench("insert(begin)" + tostr(gen), [gen](auto& st) { + auto const size = st.range(0); + std::vector in; + std::generate_n(std::back_inserter(in), size, gen); + DoNotOptimizeData(in); + + Container c(in.begin(), in.end()); + DoNotOptimizeData(c); + + ValueType value = gen(); + benchmark::DoNotOptimize(value); + + for ([[maybe_unused]] auto _ : st) { + c.insert(c.begin(), value); + DoNotOptimizeData(c); + + c.erase(std::prev(c.end())); // avoid growing indefinitely + } + }); + + if constexpr (std::random_access_iterator) { + for (auto gen : generators) + bench("insert(middle)" + tostr(gen), [gen](auto& st) { + auto const size = st.range(0); + std::vector in; + std::generate_n(std::back_inserter(in), size, gen); + DoNotOptimizeData(in); + + Container c(in.begin(), in.end()); + DoNotOptimizeData(c); + + ValueType value = gen(); + benchmark::DoNotOptimize(value); + + for ([[maybe_unused]] auto _ : st) { + auto mid = c.begin() + (size / 2); // requires random-access iterators in order to make sense + c.insert(mid, value); + DoNotOptimizeData(c); + + c.erase(c.end() - 1); // avoid growing indefinitely + } + }); + } + + if constexpr (requires(Container c) { c.reserve(0); }) { + // Insert at the start of a vector in a scenario where the vector already + // has enough capacity to hold all the elements we are inserting. + for (auto gen : generators) + bench("insert(begin, input-iter, input-iter) (no realloc)" + tostr(gen), [gen](auto& st) { + auto const size = st.range(0); + std::vector in; + std::generate_n(std::back_inserter(in), size, gen); + DoNotOptimizeData(in); + auto first = in.data(); + auto last = in.data() + in.size(); + + const int small = 100; // arbitrary + Container c; + c.reserve(size + small); // ensure no reallocation + std::generate_n(std::back_inserter(c), small, gen); + + for ([[maybe_unused]] auto _ : st) { + c.insert(c.begin(), cpp17_input_iterator(first), cpp17_input_iterator(last)); + DoNotOptimizeData(c); + + st.PauseTiming(); + c.erase(c.begin() + small, c.end()); // avoid growing indefinitely + st.ResumeTiming(); + } + }); + + // Insert at the start of a vector in a scenario where the vector already + // has almost enough capacity to hold all the elements we are inserting, + // but does need to reallocate. + for (auto gen : generators) + bench("insert(begin, input-iter, input-iter) (half filled)" + tostr(gen), [gen](auto& st) { + auto const size = st.range(0); + std::vector in; + std::generate_n(std::back_inserter(in), size, gen); + DoNotOptimizeData(in); + auto first = in.data(); + auto last = in.data() + in.size(); + + const int overflow = size / 10; // 10% of elements won't fit in the vector when we insert + Container c; + for ([[maybe_unused]] auto _ : st) { + st.PauseTiming(); + c = Container(); + c.reserve(size); + std::generate_n(std::back_inserter(c), overflow, gen); + st.ResumeTiming(); + + c.insert(c.begin(), cpp17_input_iterator(first), cpp17_input_iterator(last)); + DoNotOptimizeData(c); + } + }); + + // Insert at the start of a vector in a scenario where the vector can fit a few + // more elements, but needs to reallocate almost immediately to fit the remaining + // elements. + for (auto gen : generators) + bench("insert(begin, input-iter, input-iter) (near full)" + tostr(gen), [gen](auto& st) { + auto const size = st.range(0); + std::vector in; + std::generate_n(std::back_inserter(in), size, gen); + DoNotOptimizeData(in); + auto first = in.data(); + auto last = in.data() + in.size(); + + auto const overflow = 9 * (size / 10); // 90% of elements won't fit in the vector when we insert + Container c; + for ([[maybe_unused]] auto _ : st) { + st.PauseTiming(); + c = Container(); + c.reserve(size); + std::generate_n(std::back_inserter(c), overflow, gen); + st.ResumeTiming(); + + c.insert(c.begin(), cpp17_input_iterator(first), cpp17_input_iterator(last)); + DoNotOptimizeData(c); + } + }); + } + + ///////////////////////// + // Variations of push_back + ///////////////////////// + static constexpr bool has_push_back = requires(Container c, ValueType v) { c.push_back(v); }; + static constexpr bool has_capacity = requires(Container c) { c.capacity(); }; + static constexpr bool has_reserve = requires(Container c) { c.reserve(0); }; + if constexpr (has_push_back) { + if constexpr (has_capacity) { + // For containers where we can observe capacity(), push_back a single element + // without reserving to ensure the container needs to grow + for (auto gen : generators) + bench("push_back() (growing)" + tostr(gen), [gen](auto& st) { + auto const size = st.range(0); + std::vector in; + std::generate_n(std::back_inserter(in), size, gen); + DoNotOptimizeData(in); + + auto at_capacity = [](Container c) { + while (c.size() < c.capacity()) + c.push_back(c.back()); + return c; + }; + + std::vector c(BatchSize, at_capacity(Container(in.begin(), in.end()))); + std::vector const original = c; + + while (st.KeepRunningBatch(BatchSize)) { + for (std::size_t i = 0; i != BatchSize; ++i) { + c[i].push_back(in[i]); + DoNotOptimizeData(c[i]); + } + + st.PauseTiming(); + for (std::size_t i = 0; i != BatchSize; ++i) { + c[i] = at_capacity(Container(in.begin(), in.end())); + assert(c[i].size() == c[i].capacity()); + } + st.ResumeTiming(); + } + }); + } + + // For containers where we can reserve, push_back a single element after reserving to + // ensure the container doesn't grow + if constexpr (has_reserve) { + for (auto gen : generators) + bench("push_back() (with reserve)" + tostr(gen), [gen](auto& st) { + auto const size = st.range(0); + std::vector in; + std::generate_n(std::back_inserter(in), size, gen); + DoNotOptimizeData(in); + + Container c(in.begin(), in.end()); + // Ensure the container has enough capacity + c.reserve(c.size() + BatchSize); + DoNotOptimizeData(c); + + while (st.KeepRunningBatch(BatchSize)) { + for (std::size_t i = 0; i != BatchSize; ++i) { + c.push_back(in[i]); + } + DoNotOptimizeData(c); + + st.PauseTiming(); + c.erase(c.end() - BatchSize, c.end()); + st.ResumeTiming(); + } + }); + } + + // push_back many elements: this is amortized constant for std::vector but not all containers + for (auto gen : generators) + bench("push_back() (many elements)" + tostr(gen), [gen](auto& st) { + auto const size = st.range(0); + std::vector in; + std::generate_n(std::back_inserter(in), size, gen); + DoNotOptimizeData(in); + + Container c; + DoNotOptimizeData(c); + while (st.KeepRunningBatch(size)) { + for (int i = 0; i != size; ++i) { + c.push_back(in[i]); + } + DoNotOptimizeData(c); + + st.PauseTiming(); + c.clear(); + st.ResumeTiming(); + } + }); + } + + ///////////////////////// + // Erasure + ///////////////////////// + for (auto gen : generators) + bench("erase(begin)" + tostr(gen), [gen](auto& st) { + auto const size = st.range(0); + std::vector in; + std::generate_n(std::back_inserter(in), size, gen); + DoNotOptimizeData(in); + + Container c(in.begin(), in.end()); + DoNotOptimizeData(c); + + ValueType value = gen(); + benchmark::DoNotOptimize(value); + + for ([[maybe_unused]] auto _ : st) { + c.erase(c.begin()); + DoNotOptimizeData(c); + + c.insert(c.end(), value); // re-insert an element at the end to avoid needing a new container + } + }); + + if constexpr (std::random_access_iterator) { + for (auto gen : generators) + bench("erase(middle)" + tostr(gen), [gen](auto& st) { + auto const size = st.range(0); + std::vector in; + std::generate_n(std::back_inserter(in), size, gen); + DoNotOptimizeData(in); + + Container c(in.begin(), in.end()); + DoNotOptimizeData(c); + + ValueType value = gen(); + benchmark::DoNotOptimize(value); + + for ([[maybe_unused]] auto _ : st) { + auto mid = c.begin() + (size / 2); + c.erase(mid); + DoNotOptimizeData(c); + + c.insert(c.end(), value); // re-insert an element at the end to avoid needing a new container + } + }); + } +} + +} // namespace support + +#endif // TEST_BENCHMARKS_CONTAINERS_SEQUENCE_SEQUENCE_CONTAINER_BENCHMARKS_H diff --git a/libcxx/test/benchmarks/containers/vector.bench.cpp b/libcxx/test/benchmarks/containers/sequence/vector.bench.cpp similarity index 73% rename from libcxx/test/benchmarks/containers/vector.bench.cpp rename to libcxx/test/benchmarks/containers/sequence/vector.bench.cpp index eef23d2981642..599db1d90fa9a 100644 --- a/libcxx/test/benchmarks/containers/vector.bench.cpp +++ b/libcxx/test/benchmarks/containers/sequence/vector.bench.cpp @@ -11,12 +11,12 @@ #include #include -#include "container_benchmarks.h" +#include "sequence_container_benchmarks.h" #include "benchmark/benchmark.h" int main(int argc, char** argv) { - ContainerBenchmarks::sequence_container_benchmarks>("std::vector"); - ContainerBenchmarks::sequence_container_benchmarks>("std::vector"); + support::sequence_container_benchmarks>("std::vector"); + support::sequence_container_benchmarks>("std::vector"); benchmark::Initialize(&argc, argv); benchmark::RunSpecifiedBenchmarks(); From 112c95e9b99bf9683155fe01812bf74e9be69a1d Mon Sep 17 00:00:00 2001 From: Louis Dionne Date: Thu, 6 Feb 2025 17:29:01 -0500 Subject: [PATCH 2/2] Use __cpp_lib_containers_ranges instead of TEST_STD_VER to increase portability --- .../containers/sequence/sequence_container_benchmarks.h | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/libcxx/test/benchmarks/containers/sequence/sequence_container_benchmarks.h b/libcxx/test/benchmarks/containers/sequence/sequence_container_benchmarks.h index d844d1d93a8ce..dcd251d6997dd 100644 --- a/libcxx/test/benchmarks/containers/sequence/sequence_container_benchmarks.h +++ b/libcxx/test/benchmarks/containers/sequence/sequence_container_benchmarks.h @@ -21,7 +21,6 @@ #include "benchmark/benchmark.h" #include "test_iterators.h" -#include "test_macros.h" #include "../../GenerateInput.h" namespace support { @@ -102,7 +101,7 @@ void sequence_container_benchmarks(std::string container) { } }); -#if TEST_STD_VER >= 23 +#if defined(__cpp_lib_containers_ranges) && __cpp_lib_containers_ranges >= 202202L for (auto gen : generators) bench("ctor(Range)" + tostr(gen), [gen](auto& st) { auto const size = st.range(0);