blob: dcd251d6997dd1e81695fc7640bb564b8116b6c2 [file] [log] [blame]
// -*- 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 <algorithm>
#include <cassert>
#include <cstddef>
#include <iterator>
#include <ranges> // for std::from_range
#include <string>
#include <type_traits>
#include <vector>
#include "benchmark/benchmark.h"
#include "test_iterators.h"
#include "../../GenerateInput.h"
namespace support {
template <class Container>
void DoNotOptimizeData(Container& c) {
if constexpr (requires { c.data(); }) {
benchmark::DoNotOptimize(c.data());
} else {
benchmark::DoNotOptimize(&c);
}
}
template <class Container>
void sequence_container_benchmarks(std::string container) {
using ValueType = typename Container::value_type;
using Generator = ValueType (*)();
Generator cheap = [] { return Generate<ValueType>::cheap(); };
Generator expensive = [] { return Generate<ValueType>::expensive(); };
auto tostr = [&](Generator gen) -> std::string {
return gen == cheap ? " (cheap elements)" : " (expensive elements)";
};
std::vector<Generator> generators;
generators.push_back(cheap);
if constexpr (!std::is_integral_v<ValueType>) {
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<Container, std::size_t>) {
// 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<ValueType> 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<ValueType> 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<ValueType> 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<ValueType>& 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<ValueType> 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<typename Container::iterator>) {
for (auto gen : generators)
bench("insert(middle)" + tostr(gen), [gen](auto& st) {
auto const size = st.range(0);
std::vector<ValueType> 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<ValueType> 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<ValueType> 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<ValueType> 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<ValueType> 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<Container> c(BatchSize, at_capacity(Container(in.begin(), in.end())));
std::vector<Container> 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<ValueType> 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<ValueType> 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<ValueType> 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<typename Container::iterator>) {
for (auto gen : generators)
bench("erase(middle)" + tostr(gen), [gen](auto& st) {
auto const size = st.range(0);
std::vector<ValueType> 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