cb424d7448
verify-patch-sanity.py validates every active recipe .patch has internally- consistent hunk line counts — catching the 'malformed patch at line N' failure at commit/CI/preflight time instead of hours into a cook. This cycle hit that class three times (qtwaylandscanner, sddm, xwayland), each only discovered when cookbook tried to apply the patch. Running it across the repo found 29 latent malformed patches (validated against GNU patch: e.g. relibc/P3-sysv-ipc reproduces 'malformed patch at line 22'). They were harmless only because they sit in vendored recipes (baked, not re- applied) — but would fail on any version-bump re-derivation. --fix recounts the hunk headers (body untouched) and repaired all 29. Wired into build-preflight.sh (Phase 1.0D) and redbear-ci.yml, with a unit test (test-patch-sanity.sh). Skips archived/legacy trees and unvalidatable formats (empty placeholders, bare-@@ git hunks).
330 lines
12 KiB
C++
330 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>
|
|
|
|
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 MinDuration = std::chrono::seconds(0);
|
|
// The maximum time for which the benchmark is running.
|
|
Duration MaxDuration = std::chrono::seconds(10);
|
|
// The number of iterations in the first sample.
|
|
uint32_t InitialIterations = 1;
|
|
// The maximum number of iterations for any given sample.
|
|
uint32_t MaxIterations = 10000000;
|
|
// The minimum number of samples.
|
|
uint32_t MinSamples = 4;
|
|
// The maximum number of samples.
|
|
uint32_t MaxSamples = 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 ScalingFactor = 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 LastSampleIterations;
|
|
Duration LastBatchElapsed;
|
|
BenchmarkStatus CurrentStatus;
|
|
Duration CurrentBestGuess; // The time estimation for a single run of `foo`.
|
|
double ChangeRatio; // The change in time estimation between previous and
|
|
// current samples.
|
|
};
|
|
|
|
// A lightweight result for a benchmark.
|
|
struct BenchmarkResult {
|
|
BenchmarkStatus TerminationStatus = BenchmarkStatus::Running;
|
|
Duration BestGuess = {};
|
|
std::optional<llvm::SmallVector<BenchmarkState, 16>> MaybeBenchmarkLog;
|
|
};
|
|
|
|
// 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 NumSharing; // The number of processing units (Hyper-Threading Thread)
|
|
// with which this cache is shared.
|
|
};
|
|
|
|
// Stores information about the host.
|
|
struct HostState {
|
|
std::string CpuName; // returns a string compatible with the -march option.
|
|
double CpuFrequency; // 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 TotalTime = {};
|
|
size_t TotalIterations = 0;
|
|
|
|
public:
|
|
Duration update(const Measurement &M) {
|
|
assert(M.Iterations > 0);
|
|
// Duration is encoded as a double (see definition).
|
|
// `TotalTime` and `M.Elapsed` are of the same magnitude so we don't expect
|
|
// loss of precision due to radically different scales.
|
|
TotalTime += M.Elapsed;
|
|
TotalIterations += M.Iterations;
|
|
return TotalTime / TotalIterations;
|
|
}
|
|
};
|
|
|
|
// This class tracks the progression of the runtime estimation.
|
|
class RuntimeEstimationProgression {
|
|
RefinableRuntimeEstimation RRE;
|
|
|
|
public:
|
|
Duration CurrentEstimation = {};
|
|
|
|
// Returns the change ratio between our best guess so far and the one from the
|
|
// new measurement.
|
|
double computeImprovement(const Measurement &M) {
|
|
const Duration NewEstimation = RRE.update(M);
|
|
const double Ratio = fabs(((CurrentEstimation / NewEstimation) - 1.0));
|
|
CurrentEstimation = NewEstimation;
|
|
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 TotalBenchmarkDuration = {};
|
|
size_t Iterations = std::max(Options.InitialIterations, uint32_t(1));
|
|
size_t Samples = 0;
|
|
if (Options.ScalingFactor < 1.0)
|
|
report_fatal_error("ScalingFactor should be >= 1");
|
|
if (Options.Log != BenchmarkLog::None)
|
|
Result.MaybeBenchmarkLog.emplace();
|
|
for (;;) {
|
|
// Request a new Batch of size `Iterations`.
|
|
const auto &Batch = PP.generateBatch(Iterations);
|
|
|
|
// Measuring this Batch.
|
|
const auto StartTime = Clock.now();
|
|
for (const auto Parameter : Batch) {
|
|
auto Production = foo(Parameter);
|
|
benchmark::DoNotOptimize(Production);
|
|
}
|
|
const auto EndTime = Clock.now();
|
|
const Duration Elapsed = EndTime - StartTime;
|
|
|
|
// Updating statistics.
|
|
++Samples;
|
|
TotalBenchmarkDuration += Elapsed;
|
|
const double ChangeRatio = REP.computeImprovement({Iterations, Elapsed});
|
|
Result.BestGuess = REP.CurrentEstimation;
|
|
|
|
// Stopping condition.
|
|
if (TotalBenchmarkDuration >= Options.MinDuration &&
|
|
Samples >= Options.MinSamples && ChangeRatio < Options.Epsilon)
|
|
Result.TerminationStatus = BenchmarkStatus::PrecisionReached;
|
|
else if (Samples >= Options.MaxSamples)
|
|
Result.TerminationStatus = BenchmarkStatus::MaxSamplesReached;
|
|
else if (TotalBenchmarkDuration >= Options.MaxDuration)
|
|
Result.TerminationStatus = BenchmarkStatus::MaxDurationReached;
|
|
else if (Iterations >= Options.MaxIterations)
|
|
Result.TerminationStatus = BenchmarkStatus::MaxIterationsReached;
|
|
|
|
if (Result.MaybeBenchmarkLog) {
|
|
auto &BenchmarkLog = *Result.MaybeBenchmarkLog;
|
|
if (Options.Log == BenchmarkLog::Last && !BenchmarkLog.empty())
|
|
BenchmarkLog.pop_back();
|
|
BenchmarkState BS;
|
|
BS.LastSampleIterations = Iterations;
|
|
BS.LastBatchElapsed = Elapsed;
|
|
BS.CurrentStatus = Result.TerminationStatus;
|
|
BS.CurrentBestGuess = Result.BestGuess;
|
|
BS.ChangeRatio = ChangeRatio;
|
|
BenchmarkLog.push_back(BS);
|
|
}
|
|
|
|
if (Result.TerminationStatus != BenchmarkStatus::Running)
|
|
return Result;
|
|
|
|
if (Options.ScalingFactor > 1 &&
|
|
Iterations * Options.ScalingFactor == Iterations)
|
|
report_fatal_error(
|
|
"`Iterations *= ScalingFactor` is idempotent, increase ScalingFactor "
|
|
"or InitialIterations.");
|
|
|
|
Iterations *= Options.ScalingFactor;
|
|
}
|
|
}
|
|
|
|
// 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
|