// -*- 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 "../../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 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); 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