Skip to content
Open
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
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