How time.perf_counter_ns Avoids Float Precision Loss
Accurate micro-benchmarking in Python requires measuring execution
intervals that span only nanoseconds or microseconds. While
time.perf_counter() has traditionally been used for this
purpose, it returns a floating-point number representing seconds, which
progressively loses resolution as system uptime grows. The
time.perf_counter_ns() function addresses this limitation
by returning the elapsed time as an exact integer number of nanoseconds,
bypassing the inherent hardware limitations of floating-point
representation.
The Problem with 64-bit Floating-Point Representation
Python represents standard float values using IEEE 754
double-precision 64-bit binary numbers. These 64 bits are partitioned
into three components: a sign bit, an 11-bit exponent, and a 52-bit
significand (mantissa), providing roughly 15 to 17 decimal digits of
precision.
When time.perf_counter() queries the system clock, it
measures time from an arbitrary reference point—often the moment the
system booted or the CPU initialized. As uptime increases, the magnitude
of the whole-number seconds increases:
- At 1 second of uptime, a 64-bit float can resolve increments down to fractions of a nanosecond.
- At 100 days of uptime (approximately \(8.64 \times 10^6\) seconds), significant digits are consumed by the large integer component, forcing the fractional precision to degrade to tens of nanoseconds or worse.
When benchmarking an operation that executes in 5 nanoseconds on a machine with long uptime, the difference between the start and end timestamps can be smaller than the least significant bit of the float representation. This results in rounding errors, zero-duration readings, or step-wise quantization artifacts.
The PEP 564 Solution: Arbitrary-Precision Integers
Introduced in PEP 564, time.perf_counter_ns() resolves
precision loss by bypassing floating-point mechanics entirely:
- Integer Representation: Rather than dividing the
internal clock ticks by a frequency factor to return fractional seconds,
Python queries the underlying operating system's highest-resolution
clock (such as
QueryPerformanceCounteron Windows orclock_gettime(CLOCK_MONOTONIC)on Linux) and calculates the result strictly in integer nanoseconds. - Arbitrary Precision: Python integers
(
int) have arbitrary precision. Unlike fixed-width integers in C, Python integers dynamically allocate memory to accommodate numbers of any size without overflowing. - No Mantissa Exhaustion: Because integers do not use
an exponent-mantissa trade-off, an integer increment of
1always represents exactly one nanosecond, regardless of whether the system has been running for three seconds or three years.
Impact on Micro-Benchmarking
When conducting micro-benchmarks, measurements rely on calculating the delta between two points in time:
start = time.perf_counter_ns()
# Target micro-operation
end = time.perf_counter_ns()
duration_ns = end - startBecause start and end are integers, the
subtraction operation (end - start) is exact and immune to
catastrophic cancellation—a common issue in numerical computing where
subtracting two nearly equal floating-point numbers amplifies rounding
errors. By keeping measurements in nanosecond integer units until the
benchmark completes, developers can calculate reliable statistical
distributions (mean, median, standard deviation) for nanosecond-scale
code paths without clock-induced noise.