Don't instantiate the default implementation of benchmarks in every TU

The underlying benchmarking machinery is templated, so that it can use
different clock implementations (either for testing, or to support user
with platform-specific clocks). However, 99.99% of all users interact
with it using the `BENCHMARK` macro, which means that they use it with
`std::chrono::steady_clock`.

By adding outlined implementation of benchmarking helpers specialized
onto `std::chrono::steady_clock`, we save some amount of time per every
TU that uses benchmarks.
This commit is contained in:
Martin Hořeňovský
2026-08-22 19:21:37 +02:00
parent 77b232fba2
commit fe977e4e8c
3 changed files with 143 additions and 12 deletions
+1
View File
@@ -32,6 +32,7 @@ set(BENCHMARK_HEADERS
${SOURCES_DIR}/benchmark/detail/catch_timing.hpp
)
set(BENCHMARK_SOURCES
${SOURCES_DIR}/benchmark/catch_benchmark.cpp
${SOURCES_DIR}/benchmark/catch_chronometer.cpp
${SOURCES_DIR}/benchmark/detail/catch_analyse.cpp
${SOURCES_DIR}/benchmark/detail/catch_benchmark_function.cpp
+54
View File
@@ -0,0 +1,54 @@
// Copyright Catch2 Authors
// Distributed under the Boost Software License, Version 1.0.
// (See accompanying file LICENSE.txt or copy at
// https://www.boost.org/LICENSE_1_0.txt)
// SPDX-License-Identifier: BSL-1.0
#include <catch2/benchmark/catch_benchmark.hpp>
#include <algorithm>
#include <chrono>
#include <cmath>
namespace Catch {
namespace Benchmark {
namespace Detail {
Environment measure_environment_default() {
return Detail::measure_environment<default_clock>();
}
ExecutionPlan prepare_default( const IConfig& cfg,
Environment env,
BenchmarkFunction&& fun ) {
// This mirrors Benchmark::prepare<default_clock>(), but with the
// clock fixed so it is instantiated once here in the library.
auto min_time = env.clock_resolution.mean * Detail::minimum_ticks;
auto run_time = std::max(
min_time,
std::chrono::duration_cast<decltype( min_time )>(
cfg.benchmarkWarmupTime() ) );
auto&& test = Detail::run_for_at_least<default_clock>(
std::chrono::duration_cast<IDuration>( run_time ), 1, fun );
int new_iters = static_cast<int>(
std::ceil( min_time * test.iterations / test.elapsed ) );
return { new_iters,
test.elapsed / test.iterations * new_iters *
cfg.benchmarkSamples(),
CATCH_MOVE( fun ),
std::chrono::duration_cast<FDuration>(
cfg.benchmarkWarmupTime() ),
Detail::warmup_iterations };
}
std::vector<FDuration> run_plan_default( ExecutionPlan const& plan,
const IConfig& cfg,
Environment env ) {
return plan.run<default_clock>( cfg, env );
}
} // namespace Detail
} // namespace Benchmark
} // namespace Catch
+88 -12
View File
@@ -33,9 +33,94 @@
#include <exception>
#include <string>
#include <cmath>
#include <type_traits>
namespace Catch {
namespace Benchmark {
namespace Detail {
template <typename Clock>
ExecutionPlan prepare( const IConfig& cfg,
Environment env,
BenchmarkFunction&& fun ) {
auto min_time =
env.clock_resolution.mean * Detail::minimum_ticks;
auto run_time =
std::max( min_time,
std::chrono::duration_cast<decltype( min_time )>(
cfg.benchmarkWarmupTime() ) );
auto&& test = Detail::run_for_at_least<Clock>(
std::chrono::duration_cast<IDuration>( run_time ), 1, fun );
int new_iters = static_cast<int>(
std::ceil( min_time * test.iterations / test.elapsed ) );
return { new_iters,
test.elapsed / test.iterations * new_iters *
cfg.benchmarkSamples(),
CATCH_MOVE( fun ),
std::chrono::duration_cast<FDuration>(
cfg.benchmarkWarmupTime() ),
Detail::warmup_iterations };
}
// These are wrappers for their respective function templated
// over `default_clock`. This allows outlining the usual use
// of the template into single TU and save on compilation costs.
Environment measure_environment_default();
ExecutionPlan prepare_default( const IConfig& cfg,
Environment env,
BenchmarkFunction&& fun );
std::vector<FDuration> run_plan_default( ExecutionPlan const& plan,
const IConfig& cfg,
Environment env );
template <typename Clock>
std::enable_if_t<std::is_same<Clock, default_clock>::value,
Environment>
measureEnvironmentDispatch() {
return measure_environment_default();
}
template <typename Clock>
std::enable_if_t<!std::is_same<Clock, default_clock>::value,
Environment>
measureEnvironmentDispatch() {
return measure_environment<Clock>();
}
template <typename Clock>
std::enable_if_t<std::is_same<Clock, default_clock>::value,
std::vector<FDuration>>
runPlanDispatch( ExecutionPlan const& plan,
const IConfig& cfg,
Environment env ) {
return run_plan_default( plan, cfg, env );
}
template <typename Clock>
std::enable_if_t<!std::is_same<Clock, default_clock>::value,
std::vector<FDuration>>
runPlanDispatch( ExecutionPlan const& plan,
const IConfig& cfg,
Environment env ) {
return plan.template run<Clock>( cfg, env );
}
template <typename Clock>
std::enable_if_t<std::is_same<Clock, default_clock>::value,
ExecutionPlan>
prepareDispatch( const IConfig& cfg,
Environment env,
BenchmarkFunction&& fun ) {
return prepare_default( cfg, env, CATCH_MOVE( fun ) );
}
template <typename Clock>
std::enable_if_t<!std::is_same<Clock, default_clock>::value,
ExecutionPlan>
prepareDispatch( const IConfig& cfg,
Environment env,
BenchmarkFunction&& fun ) {
return prepare<Clock>( cfg, env, CATCH_MOVE( fun ) );
}
} // namespace Detail
struct Benchmark {
Benchmark(std::string&& benchmarkName)
: name(CATCH_MOVE(benchmarkName)) {}
@@ -44,27 +129,18 @@ namespace Catch {
Benchmark(std::string&& benchmarkName , FUN &&func)
: fun(CATCH_MOVE(func)), name(CATCH_MOVE(benchmarkName)) {}
template <typename Clock>
ExecutionPlan prepare(const IConfig &cfg, Environment env) {
auto min_time = env.clock_resolution.mean * Detail::minimum_ticks;
auto run_time = std::max(min_time, std::chrono::duration_cast<decltype(min_time)>(cfg.benchmarkWarmupTime()));
auto&& test = Detail::run_for_at_least<Clock>(std::chrono::duration_cast<IDuration>(run_time), 1, fun);
int new_iters = static_cast<int>(std::ceil(min_time * test.iterations / test.elapsed));
return { new_iters, test.elapsed / test.iterations * new_iters * cfg.benchmarkSamples(), CATCH_MOVE(fun), std::chrono::duration_cast<FDuration>(cfg.benchmarkWarmupTime()), Detail::warmup_iterations };
}
template <typename Clock = default_clock>
void run() {
static_assert( Clock::is_steady,
"Benchmarking clock should be steady" );
auto const* cfg = getCurrentContext().getConfig();
auto env = Detail::measure_environment<Clock>();
auto env = Detail::measureEnvironmentDispatch<Clock>();
getResultCapture().benchmarkPreparing(name);
CATCH_TRY{
auto plan = user_code([&] {
return prepare<Clock>(*cfg, env);
return Detail::prepareDispatch<Clock>( *cfg, env, CATCH_MOVE(fun) );
});
BenchmarkInfo info {
@@ -80,7 +156,7 @@ namespace Catch {
getResultCapture().benchmarkStarting(info);
auto samples = user_code([&] {
return plan.template run<Clock>(*cfg, env);
return Detail::runPlanDispatch<Clock>( plan, *cfg, env );
});
auto analysis = Detail::analyse(*cfg, samples.data(), samples.data() + samples.size());