Update benchmark to allow it to be built in runtimes driven mode. It is still useful to have an in-tree benchmark suite. On my side, I am working on some allocators related benchmarks. Due to the broken state of this in-tree benchmark suite, I have to frequently port changes between two workspace. Also, having benchmark suite can make it easier to track performance change in a consistent view. Assisted-by: AI tools, checked manually
338 lines
12 KiB
C++
338 lines
12 KiB
C++
//===-- Benchmark function --------------------------------------*- 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
|
|
//
|
|
//===----------------------------------------------------------------------===//
|
|
|
|
// This file mainly defines a `Benchmark` function.
|
|
//
|
|
// The benchmarking process is as follows:
|
|
// - We start by measuring the time it takes to run the function
|
|
// `InitialIterations` times. This is called a Sample. From this we can derive
|
|
// the time it took to run a single iteration.
|
|
//
|
|
// - We repeat the previous step with a greater number of iterations to lower
|
|
// the impact of the measurement. We can derive a more precise estimation of the
|
|
// runtime for a single iteration.
|
|
//
|
|
// - Each sample gives a more accurate estimation of the runtime for a single
|
|
// iteration but also takes more time to run. We stop the process when:
|
|
// * The measure stabilize under a certain precision (Epsilon),
|
|
// * The overall benchmarking time is greater than MaxDuration,
|
|
// * The overall sample count is greater than MaxSamples,
|
|
// * The last sample used more than MaxIterations iterations.
|
|
//
|
|
// - We also makes sure that the benchmark doesn't run for a too short period of
|
|
// time by defining MinDuration and MinSamples.
|
|
|
|
#ifndef LLVM_LIBC_UTILS_BENCHMARK_BENCHMARK_H
|
|
#define LLVM_LIBC_UTILS_BENCHMARK_BENCHMARK_H
|
|
|
|
#include "benchmark/benchmark.h"
|
|
#include "llvm/ADT/ArrayRef.h"
|
|
#include "llvm/ADT/SmallVector.h"
|
|
#include <array>
|
|
#include <chrono>
|
|
#include <cmath>
|
|
#include <cstdint>
|
|
#include <optional>
|
|
|
|
#include "llvm/Support/ErrorHandling.h"
|
|
|
|
namespace llvm {
|
|
namespace libc_benchmarks {
|
|
|
|
using Duration = std::chrono::duration<double>;
|
|
|
|
enum class BenchmarkLog {
|
|
None, // Don't keep the internal state of the benchmark.
|
|
Last, // Keep only the last batch.
|
|
Full // Keep all iterations states, useful for testing or debugging.
|
|
};
|
|
|
|
// An object to configure the benchmark stopping conditions.
|
|
// See documentation at the beginning of the file for the overall algorithm and
|
|
// meaning of each field.
|
|
struct BenchmarkOptions {
|
|
// The minimum time for which the benchmark is running.
|
|
Duration min_duration = std::chrono::seconds(0);
|
|
// The maximum time for which the benchmark is running.
|
|
Duration max_duration = std::chrono::seconds(10);
|
|
// The number of iterations in the first sample.
|
|
uint32_t initial_iterations = 1;
|
|
// The maximum number of iterations for any given sample.
|
|
uint32_t max_iterations = 10000000;
|
|
// The minimum number of samples.
|
|
uint32_t min_samples = 4;
|
|
// The maximum number of samples.
|
|
uint32_t max_samples = 1000;
|
|
// The benchmark will stop if the relative difference between the current and
|
|
// the last estimation is less than epsilon. This is 1% by default.
|
|
double epsilon = 0.01;
|
|
// The number of iterations grows exponentially between each sample.
|
|
// Must be greater or equal to 1.
|
|
double scaling_factor = 1.4;
|
|
BenchmarkLog log = BenchmarkLog::None;
|
|
};
|
|
|
|
// The state of a benchmark.
|
|
enum class BenchmarkStatus {
|
|
Running,
|
|
MaxDurationReached,
|
|
MaxIterationsReached,
|
|
MaxSamplesReached,
|
|
PrecisionReached,
|
|
};
|
|
|
|
// The internal state of the benchmark, useful to debug, test or report
|
|
// statistics.
|
|
struct BenchmarkState {
|
|
size_t last_sample_iterations;
|
|
Duration last_batch_elapsed;
|
|
BenchmarkStatus current_status;
|
|
Duration current_best_guess; // The time estimation for a single run of `foo`.
|
|
double change_ratio; // The change in time estimation between previous and
|
|
// current samples.
|
|
};
|
|
|
|
#ifdef LIBC_BENCHMARKS_HAS_LLVM_SUPPORT
|
|
using BenchmarkLogType = llvm::SmallVector<BenchmarkState, 16>;
|
|
#else
|
|
#include <vector>
|
|
using BenchmarkLogType = std::vector<BenchmarkState>;
|
|
#endif
|
|
|
|
// A lightweight result for a benchmark.
|
|
struct BenchmarkResult {
|
|
BenchmarkStatus termination_status = BenchmarkStatus::Running;
|
|
Duration best_guess = {};
|
|
std::optional<BenchmarkLogType> maybe_benchmark_log;
|
|
};
|
|
|
|
// Stores information about a cache in the host memory system.
|
|
struct CacheInfo {
|
|
std::string type; // e.g. "Instruction", "Data", "Unified".
|
|
int level; // 0 is closest to processing unit.
|
|
int size; // In bytes.
|
|
int num_sharing; // The number of processing units (Hyper-Threading Thread)
|
|
// with which this cache is shared.
|
|
};
|
|
|
|
// Stores information about the host.
|
|
struct HostState {
|
|
std::string cpu_name; // returns a string compatible with the -march option.
|
|
double cpu_frequency; // in Hertz.
|
|
std::vector<CacheInfo> caches;
|
|
|
|
static HostState get();
|
|
};
|
|
|
|
namespace internal {
|
|
|
|
struct Measurement {
|
|
size_t iterations = 0;
|
|
Duration elapsed = {};
|
|
};
|
|
|
|
// Updates the estimation of the elapsed time for a single iteration.
|
|
class RefinableRuntimeEstimation {
|
|
Duration total_time = {};
|
|
size_t total_iterations = 0;
|
|
|
|
public:
|
|
Duration update(const Measurement &m) {
|
|
assert(m.iterations > 0);
|
|
// Duration is encoded as a double (see definition).
|
|
// `total_time` and `m.elapsed` are of the same magnitude so we don't expect
|
|
// loss of precision due to radically different scales.
|
|
total_time += m.elapsed;
|
|
total_iterations += m.iterations;
|
|
return total_time / total_iterations;
|
|
}
|
|
};
|
|
|
|
// This class tracks the progression of the runtime estimation.
|
|
class RuntimeEstimationProgression {
|
|
RefinableRuntimeEstimation rre;
|
|
|
|
public:
|
|
Duration current_estimation = {};
|
|
|
|
// Returns the change ratio between our best guess so far and the one from the
|
|
// new measurement.
|
|
double compute_improvement(const Measurement &m) {
|
|
const Duration new_estimation = rre.update(m);
|
|
const double ratio = fabs(((current_estimation / new_estimation) - 1.0));
|
|
current_estimation = new_estimation;
|
|
return ratio;
|
|
}
|
|
};
|
|
|
|
} // namespace internal
|
|
|
|
// Measures the runtime of `foo` until conditions defined by `Options` are met.
|
|
//
|
|
// To avoid measurement's imprecisions we measure batches of `foo`.
|
|
// The batch size is growing by `ScalingFactor` to minimize the effect of
|
|
// measuring.
|
|
//
|
|
// Note: The benchmark is not responsible for serializing the executions of
|
|
// `foo`. It is not suitable for measuring, very small & side effect free
|
|
// functions, as the processor is free to execute several executions in
|
|
// parallel.
|
|
//
|
|
// - Options: A set of parameters controlling the stopping conditions for the
|
|
// benchmark.
|
|
// - foo: The function under test. It takes one value and returns one value.
|
|
// The input value is used to randomize the execution of `foo` as part of a
|
|
// batch to mitigate the effect of the branch predictor. Signature:
|
|
// `ProductType foo(ParameterProvider::value_type value);`
|
|
// The output value is a product of the execution of `foo` and prevents the
|
|
// compiler from optimizing out foo's body.
|
|
// - ParameterProvider: An object responsible for providing a range of
|
|
// `Iterations` values to use as input for `foo`. The `value_type` of the
|
|
// returned container has to be compatible with `foo` argument.
|
|
// Must implement one of:
|
|
// `Container<ParameterType> generateBatch(size_t Iterations);`
|
|
// `const Container<ParameterType>& generateBatch(size_t Iterations);`
|
|
// - Clock: An object providing the current time. Must implement:
|
|
// `std::chrono::time_point now();`
|
|
template <typename Function, typename ParameterProvider,
|
|
typename BenchmarkClock = const std::chrono::high_resolution_clock>
|
|
BenchmarkResult benchmark(const BenchmarkOptions &options,
|
|
ParameterProvider &PP, Function foo,
|
|
BenchmarkClock &Clock = BenchmarkClock()) {
|
|
BenchmarkResult result;
|
|
internal::RuntimeEstimationProgression rep;
|
|
Duration total_benchmark_duration = {};
|
|
size_t iterations = std::max(options.initial_iterations, uint32_t(1));
|
|
size_t samples = 0;
|
|
if (options.scaling_factor < 1.0)
|
|
report_fatal_error("scaling_factor should be >= 1");
|
|
if (options.log != BenchmarkLog::None)
|
|
result.maybe_benchmark_log.emplace();
|
|
for (;;) {
|
|
// Request a new Batch of size `iterations`.
|
|
const auto &batch = PP.generate_batch(iterations);
|
|
|
|
// Measuring this Batch.
|
|
const auto start_time = Clock.now();
|
|
for (const auto parameter : batch) {
|
|
auto production = foo(parameter);
|
|
benchmark::DoNotOptimize(production);
|
|
}
|
|
const auto end_time = Clock.now();
|
|
const Duration elapsed = end_time - start_time;
|
|
|
|
// Updating statistics.
|
|
++samples;
|
|
total_benchmark_duration += elapsed;
|
|
const double change_ratio = rep.compute_improvement({iterations, elapsed});
|
|
result.best_guess = rep.current_estimation;
|
|
|
|
// Stopping condition.
|
|
if (total_benchmark_duration >= options.min_duration &&
|
|
samples >= options.min_samples && change_ratio < options.epsilon)
|
|
result.termination_status = BenchmarkStatus::PrecisionReached;
|
|
else if (samples >= options.max_samples)
|
|
result.termination_status = BenchmarkStatus::MaxSamplesReached;
|
|
else if (total_benchmark_duration >= options.max_duration)
|
|
result.termination_status = BenchmarkStatus::MaxDurationReached;
|
|
else if (iterations >= options.max_iterations)
|
|
result.termination_status = BenchmarkStatus::MaxIterationsReached;
|
|
|
|
if (result.maybe_benchmark_log) {
|
|
auto &benchmark_log = *result.maybe_benchmark_log;
|
|
if (options.log == BenchmarkLog::Last && !benchmark_log.empty())
|
|
benchmark_log.pop_back();
|
|
BenchmarkState bs;
|
|
bs.last_sample_iterations = iterations;
|
|
bs.last_batch_elapsed = elapsed;
|
|
bs.current_status = result.termination_status;
|
|
bs.current_best_guess = result.best_guess;
|
|
bs.change_ratio = change_ratio;
|
|
benchmark_log.push_back(bs);
|
|
}
|
|
|
|
if (result.termination_status != BenchmarkStatus::Running)
|
|
return result;
|
|
|
|
if (options.scaling_factor > 1 &&
|
|
iterations * options.scaling_factor == iterations)
|
|
report_fatal_error("`iterations *= scaling_factor` is idempotent, "
|
|
"increase scaling_factor "
|
|
"or initial_iterations.");
|
|
|
|
iterations *= options.scaling_factor;
|
|
}
|
|
}
|
|
|
|
// Interprets `Array` as a circular buffer of `Size` elements.
|
|
template <typename T> class CircularArrayRef {
|
|
llvm::ArrayRef<T> array;
|
|
size_t size;
|
|
|
|
public:
|
|
using value_type = T;
|
|
using reference = T &;
|
|
using const_reference = const T &;
|
|
using difference_type = ssize_t;
|
|
using size_type = size_t;
|
|
|
|
class const_iterator {
|
|
using iterator_category = std::input_iterator_tag;
|
|
llvm::ArrayRef<T> array;
|
|
size_t index;
|
|
size_t offset;
|
|
|
|
public:
|
|
explicit const_iterator(llvm::ArrayRef<T> array, size_t index = 0)
|
|
: array(array), index(index), offset(index % array.size()) {}
|
|
const_iterator &operator++() {
|
|
++index;
|
|
++offset;
|
|
if (offset == array.size())
|
|
offset = 0;
|
|
return *this;
|
|
}
|
|
bool operator==(const_iterator other) const { return index == other.index; }
|
|
bool operator!=(const_iterator other) const { return !(*this == other); }
|
|
const T &operator*() const { return array[offset]; }
|
|
};
|
|
|
|
CircularArrayRef(llvm::ArrayRef<T> array, size_t size)
|
|
: array(array), size(size) {
|
|
assert(array.size() > 0);
|
|
}
|
|
|
|
const_iterator begin() const { return const_iterator(array); }
|
|
const_iterator end() const { return const_iterator(array, size); }
|
|
};
|
|
|
|
// A convenient helper to produce a CircularArrayRef from an ArrayRef.
|
|
template <typename T>
|
|
CircularArrayRef<T> cycle(llvm::ArrayRef<T> array, size_t size) {
|
|
return {array, size};
|
|
}
|
|
|
|
// Creates an std::array which storage size is constrained under `Bytes`.
|
|
template <typename T, size_t Bytes>
|
|
using ByteConstrainedArray = std::array<T, Bytes / sizeof(T)>;
|
|
|
|
// A convenient helper to produce a CircularArrayRef from a
|
|
// ByteConstrainedArray.
|
|
template <typename T, size_t N>
|
|
CircularArrayRef<T> cycle(const std::array<T, N> &container, size_t size) {
|
|
return {llvm::ArrayRef<T>(container.cbegin(), container.cend()), size};
|
|
}
|
|
|
|
// Makes sure the binary was compiled in release mode and that frequency
|
|
// governor is set on performance.
|
|
void checkRequirements();
|
|
|
|
} // namespace libc_benchmarks
|
|
} // namespace llvm
|
|
|
|
#endif // LLVM_LIBC_UTILS_BENCHMARK_BENCHMARK_H
|