Skip to content
Open
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,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.16.1 - 2026-02-26
### Changed
- replaced Boost accumulator with Welford online algorithm for timer precision variance calculation
- guarantee monotonic clock (prefer `steady_clock` over `high_resolution_clock` when not steady)
- updated 95% confidence interval to use unbiased sample variance (Bessel's correction)
- increased clock precision sample size (1000 iterations)
- all clock delta samples are accepted as valid (large values on coarse-timer VMs are legitimate measurements)

## 3.16.0 - 2025-02-26
### Added
- Docker buildx recipes and scripts
Expand Down
2 changes: 1 addition & 1 deletion P11PERFTEST_VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
3.16.0
3.16.1
2 changes: 1 addition & 1 deletion src/p11perftest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -334,7 +334,7 @@ int main(int argc, char **argv)
}

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

Executor executor( testvecs, sessions, argnthreads, epsilon, generate_session_keys==true, datapoints );
// Track which keys were successfully generated
Expand Down
59 changes: 34 additions & 25 deletions src/timeprecision.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,47 +18,56 @@

#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>



using namespace std;
using namespace boost::accumulators;


// reference: https://www.statsdirect.com/help/basic_descriptive_statistics/standard_deviation.htm
// returned time is in ns
// Returned time is in nanoseconds (ns).
// Reference: https://www.statsdirect.com/help/basic_descriptive_statistics/standard_deviation.htm

pair<nanoseconds_double_t, nanoseconds_double_t> measure_clock_precision(int iter)
{
using clock = std::chrono::high_resolution_clock;
accumulator_set<double, stats<tag::mean, tag::variance, tag::count> > acc;
// 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<nanoseconds_double_t>(current - start);
acc(delta.count());

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

// All non-zero deltas are valid: a large value (e.g. 15ms on Hyper-V) is a legitimate
// measurement of the system's clock granularity, not an error.
++n;
const double delta = x - mean;
mean += delta / static_cast<double>(n);
const double delta2 = x - mean;
M2 += delta * delta2;
}

// Unbiased sample variance (requires n >= 2, which is implied at this point)
double sample_variance = M2 / static_cast<double>(n - 1);

auto n = boost::accumulators::count(acc);
// compute estimator for variance: (n)/(n-1)*variance
auto est_variance = (variance(acc) * n ) / (n-1);
// 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;

// compute standard error
// we take k=2, so 95% of measures are within interval
auto std_err = nanoseconds_double_t( sqrt( est_variance/n ) * 2);
auto avg = nanoseconds_double_t(mean(acc));

return {avg, std_err};
return {nanoseconds_double_t(mean), nanoseconds_double_t(ci_halfwidth_95)};
}
5 changes: 4 additions & 1 deletion src/timeprecision.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@
#include <utility>
#include "units.hpp"

std::pair<nanoseconds_double_t, nanoseconds_double_t> measure_clock_precision(int iter=100);
// Returns (mean_ns, 95% CI half-width in ns).
// All clock delta samples are accepted; large values on coarse-timer VMs (e.g. 15ms on Hyper-V)
// are legitimate measurements of that system's clock granularity.
std::pair<nanoseconds_double_t, nanoseconds_double_t> measure_clock_precision(int iter=1000);

#endif // TIMEPRECISION_H