Here is how libcpucycles decides which cycle counter to use. The underlying principles are as follows:
-
Failure is not allowed. Using a low-resolution timer such as
gettimeofday()
to estimate cycle counts is not desirable but is better than providing no information. -
A counter that does well on some CPUs and OSes can do badly on others. The counter selection in libcpucycles is based not just on rules set at compile time but also on measurements of how well the counters perform when the program first calls
cpucycles()
. -
A critical application of cycle counting is collecting cycle counts for multiple options to see which option is faster. It is the caller's responsibility to compute medians of cycle counts for many runs of whatever is being benchmarked: medians filter out occasional cycle-count jumps caused by migration to another core (if the benchmark is not pinned to a single core) or interrupts from other OS activity. libcpucycles does not reject an otherwise attractive counter merely because of occasional jumps.
-
Cycle-counting overhead is not desirable, but does not directly affect comparisons of multiple options measured using the same cycle counter, so it is less important than consistent major errors such as treating 2^32 + x cycles as x cycles. (Performance experts seeing a function that takes billions of cycles usually focus on smaller subroutines, but libcpucycles should not break larger measurements.) This is why libcpucycles does not provide direct access to 32-bit cycle counters: it provides wrappers that combine the counters with gettimeofday() to produce 64 bits, even though this incurs some extra overhead.
-
The noise introduced by typical off-core clocks, such as multiplying a 24MHz clock by 86 to estimate cycles on a 2.064GHz CPU core, comes in small part from low resolution but much more from changes in CPU frequency: e.g., a 10000-cycle computation might be measured as 20000 cycles when the CPU enters a power-saving mode. When libcpucycles has access to what is believed to be an on-core cycle counter, it uses that even when its measurements show some noise. (Choosing an on-core cycle counter does not magically eliminate the change in the relative speed of the CPU and DRAM; the usual advice to warm up the CPU and set constant frequencies if possible still applies.)
When cpucycles()
is first called, libcpucycles tries running each
cycle counter that has been compiled into the library. For example, for
64-bit ARM CPUs, libcpucycles will try arm64-pmc
, arm64-vct
,
default-gettimeofday
, default-mach
, default-monotonic
, and
default-perfevent
, minus any of those that failed to compile.
Cycle counters that fail at run time with SIGILL (or SIGFPE or SIGBUS or
SIGSEGV) are eliminated from the list. For example, arm64-pmc
will
fail with SIGILL if the kernel does not allow user access to
PMCCNTR_EL0
. Beware that libcpucycles does not catch SIGILL after its
initial tests: if the kernel initially allows user access to
PMCCNTR_EL0
but later turns it off then arm64-pmc
will crash.
Independently of these counters, libcpucycles uses various OS mechanisms
to obtain an estimate of the CPU frequency. This estimate is also
available to the caller as cpucycles_persecond()
.
The methods that libcpucycles uses to ask the OS for an estimated CPU
frequency fail on some OS-CPU combinations, in which case libcpucycles
falls back to a cpucyclespersecond
environment variable, or, if that
variable does not exist, an estimate of 2399987654 cycles per second.
(This estimate is in a realistic range of CPU speeds, and is close to
multiples of 24MHz, 25MHz, and 19.2MHz, which are common crystal
frequencies.) The sysadmin can create /etc/cpucyclespersecond
to
override all of the OS mechanisms.
For counters that do not ask for scaling, the estimated CPU frequency is
shown in cpucycles-info
as a double-check on the counter results. For
counters that ask for scaling, libcpucycles uses the estimated CPU
frequency to compute the scaling, so this is not a double-check. If a
counter asks for scaling and the estimated CPU frequency does not seem
close to a multiple of the counter frequency (possibly with a small
power-of-2 denominator) then libcpucycles will throw the counter away,
except in the case of fixed-resolution OS counters such as
gettimeofday
and CLOCK_MONOTONIC
.
libcpucycles computes a precision estimate for each counter (times any applicable scaling) as follows. Call the counter 1000 times. Check that the counter has never decreased, and has increased at least once. (A counter where the decrease/increase checks fail is retried 10 times, so 10000 calls overall, and removed if it fails all 10 times.) The precision estimate is then the smallest nonzero difference between adjacent counter results, plus a penalty explained below.
The penalty is 100 cycles for off-core counters (including RDTSC) and
default-perfevent
, and 200 cycles for fixed-resolution OS counters.
For example, an on-core CPU cycle counter will be selected even if it
actually has, e.g., a resolution of 8 cycles and 50 cycles of overhead.
Finally, libcpucycles selects the counter where the precision estimate is the smallest number of cycles. Note that an inaccurate estimate of CPU frequency can influence the choice between a scaled counter and an unscaled counter.
libcpucycles does not carry out its counter selection (typically tens
of milliseconds, sometimes even more) as a static initializer; callers
are presumed to not want to incur the cost of initialization unless and
until they are actually using cpucycles()
. A multithreaded caller thus
has to place locks around any possibly-first call to cpucycles()
, or
create its own static initializer (an __attribute__((constructor))
function) with an initial cpucycles()
call so that all subsequent
cpucycles()
calls are thread-safe.
Version: This is version 2023.01.05 of the "Selection" web page.