Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).

## 3.15.2 - 2025-12-12
### Updated
- removed Welford variance calculation boost dependency for timer precision (using standard lib)
- guarantee monotonic clock
- updated confidence interval calculation for sample variance
- increased sample size
- safety checks

## 3.15.1 - 2025-11-26
### Fixed
- precision of timers was not right on some virtual environments, that may lead to improper rounding of results
Expand Down
2 changes: 1 addition & 1 deletion P11PERFTEST_VERSION
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's reset release tagging - I prefer to keep this in a separate commit. The 3.15.1 was a mistake and It will be corrected through PR #21.

Original file line number Diff line number Diff line change
@@ -1 +1 @@
3.15.1
3.15.2
2 changes: 1 addition & 1 deletion src/p11perftest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -320,7 +320,7 @@ int main(int argc, char **argv)
}

auto epsilon = measure_clock_precision();
std::cout << std::endl << "timer granularity (ns): " << epsilon.first << " +/- " << epsilon.second << "\n\n";
std::cout << std::endl << "timer granularity (ns): " << epsilon.first << " +/- " << epsilon.second << " (95% confidence interval)\n\n";

Executor executor( testvecs, sessions, argnthreads, epsilon, generate_session_keys==true );

Expand Down
68 changes: 45 additions & 23 deletions src/timeprecision.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -28,46 +28,68 @@

#include "timeprecision.hpp"

#include <iostream>
#include <chrono>
#include <cmath>
#include <boost/accumulators/accumulators.hpp>
#include <boost/accumulators/statistics/stats.hpp>
#include <boost/accumulators/statistics/mean.hpp>
#include <boost/accumulators/statistics/count.hpp>
#include <boost/accumulators/statistics/variance.hpp>


#include <cstdlib>

using namespace std;
using namespace boost::accumulators;


// reference: https://www.statsdirect.com/help/basic_descriptive_statistics/standard_deviation.htm
// returned time is in ns
// TODO: using litterals for setting units
// Returned time is in nanoseconds (ns).
// Reference: https://www.statsdirect.com/help/basic_descriptive_statistics/standard_deviation.htm
// TODO: use chrono literals for units if/when interface becomes typed

pair<double, double> measure_clock_precision(int iter)
{
using clock = std::chrono::high_resolution_clock;
accumulator_set<double, stats<tag::mean, tag::variance, tag::count> > te;
// Guard against non-monotonic high_resolution_clock (may alias system_clock) at compile-time
using clock = std::conditional_t<
std::chrono::high_resolution_clock::is_steady,
std::chrono::high_resolution_clock,
std::chrono::steady_clock>;

// Welford's online algorithm for stable mean/variance
double mean = 0.0;
double M2 = 0.0;
int n = 0;

for (int i = 0; i < iter; ++i) {
auto start = clock::now();
auto start = clock::now();
auto current = start;

// Probe until the time point changes
while (current == start) {
current = clock::now();
}
const auto delta = std::chrono::duration_cast<std::chrono::nanoseconds>(current - start).count();
te(static_cast<double>(delta));

const auto delta_ns =
std::chrono::duration_cast<std::chrono::nanoseconds>(current - start).count();
const double x = static_cast<double>(delta_ns);

// Filter out unrealistic values (likely measurement errors)
// Timer granularity should be < 1ms on modern systems
if (x > 0.0 && x < 1'000'000.0) { // between 0 and 1 ms (in nanoseconds)
++n;
const double delta = x - mean;
mean += delta / static_cast<double>(n);
const double delta2 = x - mean;
M2 += delta * delta2;
} else {
std::cerr << "Warning: Filtered out unrealistic timer value: " << delta_ns << " ns\n";
}
}

// Kill the app if insufficient number of valid samples
if (n < 100) {
std::cerr << "Fatal error: Insufficient valid samples (" << n << "). Exiting.\n";
std::exit(EXIT_FAILURE); // terminate the program with failure status
}

auto n = boost::accumulators::count(te);
// compute estimator for variance: (n)/(n-1)*variance
auto est_variance = (variance(te) * n ) / (n-1);
// Unbiased sample variance (requires n >= 2, which is implied at this point)
double sample_variance = M2 / static_cast<double>(n - 1);

// compute standard error
double std_err = sqrt( est_variance/n ) * 2; // we take k=2, so 95% of measures are within interval
// Standard Error of the Mean (SEM) with 95% CI via normal approx: z = 1.96
// ci_halfwidth_95 = sqrt( sample_variance / n ) * 1.96
double ci_halfwidth_95 = std::sqrt(sample_variance / static_cast<double>(n)) * 1.96;

return make_pair(mean(te), std_err);
return make_pair(mean, ci_halfwidth_95);
}
6 changes: 5 additions & 1 deletion src/timeprecision.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@

#include <utility>

std::pair<double, double> measure_clock_precision(int iter=100);
// Returns (mean_ns, 95% CI half-width in ns).
// Behavior:
// - Filters out unrealistic timer values (> 1 ms).
// - Terminates the program (std::exit) if fewer than 100 valid samples remain.
std::pair<double, double> measure_clock_precision(int iter=1000);

#endif // TIMEPRECISION_H