Skip to content

Commit 36b0eeb

Browse files
fix(core): fix walltime stats computation to handle total round time as an input
The computation is now in line with that codspeed-rust does, the interface is ready for other frameworks that may different iterations count from one round to another.
1 parent 8718448 commit 36b0eeb

File tree

3 files changed

+113
-91
lines changed

3 files changed

+113
-91
lines changed

core/include/codspeed.h

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,10 @@ class CodSpeed {
3434
struct RawWalltimeBenchmark {
3535
std::string name;
3636
std::string uri;
37-
uint64_t iter_per_round;
38-
double mean_ns;
39-
double median_ns;
40-
double stdev_ns;
41-
std::vector<double> round_times_ns;
37+
// Number of iterations of each round
38+
std::vector<uint64_t> iters_per_round;
39+
// Time taken by each round, meaning all its iterations, in nanoseconds
40+
std::vector<double> times_per_round_ns;
4241
};
4342

4443
void generate_codspeed_walltime_report(

core/src/walltime.cpp

Lines changed: 97 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
#include <algorithm>
2+
#include <cassert>
23
#include <cmath>
34
#include <cstdlib>
45
#include <filesystem>
@@ -34,6 +35,27 @@ struct BenchmarkStats {
3435
uint64_t stdev_outlier_rounds;
3536
uint64_t iter_per_round;
3637
uint64_t warmup_iters;
38+
39+
BenchmarkStats(double min_ns = 0.0, double max_ns = 0.0, double mean_ns = 0.0,
40+
double stdev_ns = 0.0, double q1_ns = 0.0,
41+
double median_ns = 0.0, double q3_ns = 0.0,
42+
uint64_t rounds = 0, double total_time = 0.0,
43+
uint64_t iqr_outlier_rounds = 0,
44+
uint64_t stdev_outlier_rounds = 0, uint64_t iter_per_round = 0,
45+
uint64_t warmup_iters = 0)
46+
: min_ns(min_ns),
47+
max_ns(max_ns),
48+
mean_ns(mean_ns),
49+
stdev_ns(stdev_ns),
50+
q1_ns(q1_ns),
51+
median_ns(median_ns),
52+
q3_ns(q3_ns),
53+
rounds(rounds),
54+
total_time(total_time),
55+
iqr_outlier_rounds(iqr_outlier_rounds),
56+
stdev_outlier_rounds(stdev_outlier_rounds),
57+
iter_per_round(iter_per_round),
58+
warmup_iters(warmup_iters) {}
3759
};
3860

3961
struct BenchmarkMetadata {
@@ -46,51 +68,22 @@ struct CodspeedWalltimeBenchmark {
4668
BenchmarkStats stats;
4769
};
4870

49-
double compute_quantile(const std::vector<double> &data, double quantile) {
50-
size_t n = data.size();
71+
static double compute_quantile(const std::vector<double> &sorted_data,
72+
double quantile) {
73+
size_t n = sorted_data.size();
5174
if (n == 0) return 0.0;
5275

5376
double pos = quantile * (n - 1);
5477
size_t k = static_cast<size_t>(pos);
5578
double d = pos - k;
5679

5780
if (k + 1 < n) {
58-
return data[k] + d * (data[k + 1] - data[k]);
81+
return sorted_data[k] + d * (sorted_data[k + 1] - sorted_data[k]);
5982
}
60-
return data[k];
61-
}
62-
63-
void compute_iqr_and_outliers(const std::vector<double> &times_ns, double mean,
64-
double stdev, double &q1, double &q3, double &iqr,
65-
size_t &iqr_outlier_rounds,
66-
size_t &stdev_outlier_rounds) {
67-
std::vector<double> sorted_times = times_ns;
68-
std::sort(sorted_times.begin(), sorted_times.end());
69-
70-
q1 = compute_quantile(sorted_times, 0.25);
71-
q3 = compute_quantile(sorted_times, 0.75);
72-
73-
iqr = q3 - q1;
74-
75-
const double IQR_OUTLIER_FACTOR = 1.5;
76-
const double STDEV_OUTLIER_FACTOR = 3.0;
77-
78-
iqr_outlier_rounds =
79-
std::count_if(sorted_times.begin(), sorted_times.end(),
80-
[q1, q3, iqr, IQR_OUTLIER_FACTOR](double x) {
81-
return x < q1 - IQR_OUTLIER_FACTOR * iqr ||
82-
x > q3 + IQR_OUTLIER_FACTOR * iqr;
83-
});
84-
85-
stdev_outlier_rounds =
86-
std::count_if(sorted_times.begin(), sorted_times.end(),
87-
[mean, stdev, STDEV_OUTLIER_FACTOR](double x) {
88-
return x < mean - STDEV_OUTLIER_FACTOR * stdev ||
89-
x > mean + STDEV_OUTLIER_FACTOR * stdev;
90-
});
83+
return sorted_data[k];
9184
}
9285

93-
std::string escapeBackslashes(const std::string &input) {
86+
static std::string escapeBackslashes(const std::string &input) {
9487
std::string output;
9588
for (char c : input) {
9689
if (c == '\\') {
@@ -102,7 +95,7 @@ std::string escapeBackslashes(const std::string &input) {
10295
return output;
10396
}
10497

105-
void write_codspeed_benchmarks_to_json(
98+
static void write_codspeed_benchmarks_to_json(
10699
const std::vector<CodspeedWalltimeBenchmark> &benchmarks) {
107100
std::ostringstream oss;
108101

@@ -202,40 +195,80 @@ void generate_codspeed_walltime_report(
202195
std::vector<CodspeedWalltimeBenchmark> codspeed_walltime_benchmarks;
203196

204197
for (const auto &raw_benchmark : raw_walltime_benchmarks) {
205-
CodspeedWalltimeBenchmark codspeed_benchmark;
206-
codspeed_benchmark.metadata = {raw_benchmark.name, raw_benchmark.uri};
198+
assert(raw_benchmark.iters_per_round.size() ==
199+
raw_benchmark.times_per_round_ns.size());
200+
201+
// Convert total round times to per-iteration times
202+
std::vector<double> per_iteration_times_ns;
203+
for (size_t i = 0; i < raw_benchmark.times_per_round_ns.size(); i++) {
204+
double per_iter_time_ns = raw_benchmark.times_per_round_ns[i] /
205+
raw_benchmark.iters_per_round[i];
206+
per_iteration_times_ns.push_back(per_iter_time_ns);
207+
}
207208

209+
// Sort for quantile computation
210+
std::vector<double> sorted_per_iter_times_ns = per_iteration_times_ns;
211+
std::sort(sorted_per_iter_times_ns.begin(), sorted_per_iter_times_ns.end());
212+
213+
// Compute statistics from per-iteration times
214+
double mean_ns = std::accumulate(per_iteration_times_ns.begin(),
215+
per_iteration_times_ns.end(), 0.0) /
216+
per_iteration_times_ns.size();
217+
218+
double variance = 0.0;
219+
for (double time_ns : per_iteration_times_ns) {
220+
double diff = time_ns - mean_ns;
221+
variance += diff * diff;
222+
}
223+
double stdev_ns = std::sqrt(variance / per_iteration_times_ns.size());
224+
const double STDEV_OUTLIER_FACTOR = 3.0;
225+
size_t stdev_outlier_rounds = std::count_if(
226+
sorted_per_iter_times_ns.begin(), sorted_per_iter_times_ns.end(),
227+
[mean_ns, stdev_ns, STDEV_OUTLIER_FACTOR](double x) {
228+
return x < mean_ns - STDEV_OUTLIER_FACTOR * stdev_ns ||
229+
x > mean_ns + STDEV_OUTLIER_FACTOR * stdev_ns;
230+
});
231+
232+
double q1_ns = compute_quantile(sorted_per_iter_times_ns, 0.25);
233+
double median_ns = compute_quantile(sorted_per_iter_times_ns, 0.5);
234+
double q3_ns = compute_quantile(sorted_per_iter_times_ns, 0.75);
235+
236+
double iqr_ns = q3_ns - q1_ns;
237+
const double IQR_OUTLIER_FACTOR = 1.5;
238+
size_t iqr_outlier_rounds = std::count_if(
239+
sorted_per_iter_times_ns.begin(), sorted_per_iter_times_ns.end(),
240+
[q1_ns, q3_ns, iqr_ns, IQR_OUTLIER_FACTOR](double x) {
241+
return x < q1_ns - IQR_OUTLIER_FACTOR * iqr_ns ||
242+
x > q3_ns + IQR_OUTLIER_FACTOR * iqr_ns;
243+
});
244+
245+
// Compute total time in seconds
208246
double total_time =
209-
std::accumulate(raw_benchmark.round_times_ns.begin(),
210-
raw_benchmark.round_times_ns.end(), 0.0) /
247+
std::accumulate(raw_benchmark.times_per_round_ns.begin(),
248+
raw_benchmark.times_per_round_ns.end(), 0.0) /
211249
1e9;
212250

213-
double mean = raw_benchmark.mean_ns;
214-
double median = raw_benchmark.median_ns;
215-
double stdev = raw_benchmark.stdev_ns;
216-
double q1, q3, iqr;
217-
size_t iqr_outlier_rounds, stdev_outlier_rounds;
218-
compute_iqr_and_outliers(raw_benchmark.round_times_ns, mean, stdev, q1, q3,
219-
iqr, iqr_outlier_rounds, stdev_outlier_rounds);
251+
// TODO: CodSpeed format only supports one iter_per_round for all rounds,
252+
// for now take the average
253+
uint64_t avg_iters_per_round =
254+
std::accumulate(raw_benchmark.iters_per_round.begin(),
255+
raw_benchmark.iters_per_round.end(), 0ULL) /
256+
raw_benchmark.iters_per_round.size();
220257

221258
// Populate stats
222-
codspeed_benchmark.stats = {
223-
*std::min_element(raw_benchmark.round_times_ns.begin(),
224-
raw_benchmark.round_times_ns.end()),
225-
*std::max_element(raw_benchmark.round_times_ns.begin(),
226-
raw_benchmark.round_times_ns.end()),
227-
mean,
228-
stdev,
229-
q1,
230-
median,
231-
q3,
232-
raw_benchmark.round_times_ns.size(),
233-
total_time,
234-
iqr_outlier_rounds,
235-
stdev_outlier_rounds,
236-
raw_benchmark.iter_per_round,
237-
0 // TODO: warmup_iters
238-
};
259+
BenchmarkStats stats(*std::min_element(sorted_per_iter_times_ns.begin(),
260+
sorted_per_iter_times_ns.end()),
261+
*std::max_element(sorted_per_iter_times_ns.begin(),
262+
sorted_per_iter_times_ns.end()),
263+
mean_ns, stdev_ns, q1_ns, median_ns, q3_ns,
264+
raw_benchmark.times_per_round_ns.size(), total_time,
265+
iqr_outlier_rounds, stdev_outlier_rounds,
266+
avg_iters_per_round,
267+
0 // TODO: warmup_iters
268+
);
269+
CodspeedWalltimeBenchmark codspeed_benchmark;
270+
codspeed_benchmark.metadata = {raw_benchmark.name, raw_benchmark.uri};
271+
codspeed_benchmark.stats = stats;
239272

240273
codspeed_walltime_benchmarks.push_back(codspeed_benchmark);
241274
}

google_benchmark/src/benchmark.cc

Lines changed: 12 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,8 @@ void FlushStreams(BenchmarkReporter* reporter) {
382382
}
383383

384384
#ifdef CODSPEED_WALLTIME
385+
// NOTE: What Google Benchmark calls a repetition, Codspeed calls a round.
386+
//
385387
// We use real time by default, but we could offer CPU time usage as a build
386388
// option, open an issue if you need it.
387389
codspeed::RawWalltimeBenchmark generate_raw_walltime_data(
@@ -402,27 +404,15 @@ codspeed::RawWalltimeBenchmark generate_raw_walltime_data(
402404
walltime_data.uri = "unknown_file::" + walltime_data.uri;
403405
}
404406

405-
walltime_data.iter_per_round = run.iterations;
406-
walltime_data.round_times_ns.push_back(run.GetAdjustedRealTime());
407-
}
407+
// Collect iteration count for this round/repetition
408+
walltime_data.iters_per_round.push_back(
409+
static_cast<uint64_t>(run.iterations));
408410

409-
if (run_results.aggregates_only.empty()) {
410-
// If run has no aggreagates, it means that only one round was performed.
411-
// Use this time as a mean, median, and set stdev to 0.
412-
double only_round_time_ns = walltime_data.round_times_ns[0];
413-
walltime_data.mean_ns = only_round_time_ns;
414-
walltime_data.median_ns = only_round_time_ns;
415-
walltime_data.stdev_ns = 0;
416-
} else {
417-
for (const auto& aggregate_run : run_results.aggregates_only) {
418-
if (aggregate_run.aggregate_name == "mean") {
419-
walltime_data.mean_ns = aggregate_run.GetAdjustedRealTime();
420-
} else if (aggregate_run.aggregate_name == "median") {
421-
walltime_data.median_ns = aggregate_run.GetAdjustedRealTime();
422-
} else if (aggregate_run.aggregate_name == "stddev") {
423-
walltime_data.stdev_ns = aggregate_run.GetAdjustedRealTime();
424-
}
425-
}
411+
// Collect total round time in nanoseconds
412+
// real_accumulated_time is in seconds, convert to nanoseconds
413+
double round_time_ns =
414+
run.real_accumulated_time * GetTimeUnitMultiplier(kNanosecond);
415+
walltime_data.times_per_round_ns.push_back(round_time_ns);
426416
}
427417

428418
return walltime_data;
@@ -568,12 +558,12 @@ void RunBenchmarks(const std::vector<BenchmarkInstance>& benchmarks,
568558
internal::BenchmarkRunner& runner = runners[repetition_index];
569559

570560
#ifdef CODSPEED_WALLTIME
571-
auto codspeed = codspeed::CodSpeed::getInstance();
561+
auto* codspeed = codspeed::CodSpeed::getInstance();
572562
if (codspeed != nullptr) {
573563
codspeed->start_benchmark(runner.GetBenchmarkName());
574564
}
575565
#endif
576-
566+
577567
runner.DoOneRepetition();
578568
if (runner.HasRepeatsRemaining()) {
579569
continue;

0 commit comments

Comments
 (0)