skip to content

When does time.perf_counter_ns() beat time.perf_counter() for timing Python code?

level: middleimportance: should knowfreq 32%

answer

  1. The difference is the return type
  2. Floats carry a fixed number of significant digits
  3. Precision falls away as the reading grows
  4. Rounding errors accumulate over many measurements
  5. Integers are arbitrary precision in Python

basics

~20 s

time.perf_counter() returns float seconds, and a float carries only 53 bits of mantissa, so a large reading cannot resolve nanoseconds. time.perf_counter_ns() returns an exact integer count of nanoseconds, so no precision is lost to rounding.

solid answer

~50 s

Both read the same underlying high-resolution counter; they differ in the type they hand back. `time.perf_counter()` converts to a float of seconds, and a Python float has a 53-bit mantissa, so once the reading is large the gap between adjacent representable values grows: near a reading of a billion seconds that gap is about 119 nanoseconds, and any finer difference is rounded away before you ever subtract. `time.perf_counter_ns()` returns an integer number of nanoseconds, so the subtraction is exact whatever the magnitude of the reference point. That matters when you time very short operations, when you accumulate many small measurements — the rounding error of each float conversion adds up across a run — or when you want reproducible integers rather than values that drift in the last digits. For a measurement of milliseconds or more, the float form is perfectly fine and reads better.

code

python · 10 lines
python
import time

t0 = time.perf_counter_ns()
total = sum(range(340))
t1 = time.perf_counter_ns()
print(t1 - t0, "ns")  # an exact integer difference

# a float reading near 1e9 seconds cannot resolve 100 nanoseconds
reading = 1e9
print(reading + 100e-9 - reading)  # 1.1920928955078125e-07, not 1e-07

go deeper

for a junior

Know that both calls time an interval and that the _ns version returns whole nanoseconds as an integer instead of seconds as a float. Remember that only the difference of two readings means anything.

for a middle

Explain the mechanism: 53 bits of float mantissa, spacing that grows with magnitude, precision lost in the conversion before you subtract. Say when the float form is still the sensible default.

for a senior

Bring the accumulation angle. Show why summing hundreds of float durations produces a total that changes with ordering, and why integer nanoseconds make a timing harness reproducible run to run.

for a principal

Frame it as measurement hygiene: what units timings are stored and aggregated in across a codebase, where the conversion to human-readable seconds happens, and whether reported differences are above the noise floor at all.

## Same clock, different return type `time.perf_counter()` and `time.perf_counter_ns()` read the same underlying performance counter. The difference is entirely in what they give you: float seconds versus an integer count of nanoseconds. The same pairing exists for the other clocks — `time.monotonic()` / `time.monotonic_ns()` and `time.time()` / `time.time_ns()`. The `_ns` family exists for exactly one reason, and an interviewer asking this question is checking whether you know what that reason is. ## Why a float loses nanoseconds A Python float is an IEEE-754 double: one sign bit, 11 exponent bits, and 52 stored mantissa bits giving 53 bits of significand. That is about 15–16 significant decimal digits *in total*, regardless of magnitude. The absolute spacing between adjacent representable values — the ULP, "unit in the last place" — therefore grows with the magnitude of the number: * near 1 second, the spacing is about 2.2e-16 s, far finer than any clock * near 1e6 seconds (~11 days), the spacing is about 1.2e-10 s, still fine * near 1e9 seconds (~31 years), the spacing is about 1.19e-7 s — **119 nanoseconds** So on a platform whose counter's reference point is far from zero, or in a process that has been running a very long time, a float clock reading simply cannot express a 100-nanosecond difference. Adding 100 ns to a reading of 1e9 does not produce a value 100 ns larger; it produces one about 119 ns larger, because that is the nearest value the format can hold. The precision was lost in the *conversion*, before you did any arithmetic, and no amount of careful subtraction gets it back. `time.perf_counter_ns()` sidesteps the whole problem. Python's integers are arbitrary precision, so a nanosecond count of any magnitude is exact, and `t1 - t0` is exact too. ## The second failure: accumulation The precision ceiling is the textbook answer. The one that bites in practice is accumulation. Consider a benchmarking harness that times each case in a 340-case regression pack for a warehouse pick-list builder, keeping a running float total and a per-case list. Each individual float conversion rounds, and each rounding error is tiny — but summing 340 of them lets the errors accumulate in a direction that is not guaranteed to cancel, so the reported total quietly disagrees with the sum you get by re-adding the same numbers in another order. Two runs of the identical pack then produce totals that differ in the last digits, and the "regression" the harness reports is floating-point drift rather than a real change in the code. Timing in integer nanoseconds removes that class of noise entirely: integers add associatively and exactly, so the total is the total, and you convert to seconds once, at the point where you print it. ## When the float form is the right call Most of the time. If you are timing something that takes milliseconds, `time.perf_counter()` reads better, composes with the rest of your arithmetic without a division by `1_000_000_000`, and its precision is orders of magnitude beyond your measurement noise anyway. The overhead of the call itself, plus interpreter dispatch, dwarfs the representation question at that scale. Reach for `_ns` when you are timing operations near the microsecond level, when you are summing a large number of measurements, or when you want exact integers to store or compare. ## Related distinctions worth having ready * **`perf_counter` vs `monotonic`.** `perf_counter` is documented as the highest-resolution clock available for measuring a short interval; `monotonic` is the one you enforce a deadline with. Both are monotonic and both include time spent sleeping. On several platforms they read the same counter, so the difference is one of contract rather than always of mechanism. * **Elapsed vs CPU time.** `time.perf_counter()` measures wall-clock elapsed time, including time blocked on I/O or asleep. If you want the CPU time the process or thread actually consumed, that is `time.process_time()` or `time.thread_time()`, and their `_ns` variants exist too. * **Resolution vs precision.** `time.get_clock_info("perf_counter").resolution` reports the clock's advertised resolution. That is a statement about the clock, not about the float you stored it in — you can lose precision the format cannot hold even when the clock could have supplied it. * **A single reading is still meaningless.** Like `monotonic`, `perf_counter` counts from an undefined reference point. `perf_counter_ns()` gives you an exact integer of *nothing in particular*; only the difference is a duration.

  • Does time.perf_counter_ns() read a finer-grained clock than time.perf_counter()?
    No. They read the same underlying counter with the same hardware resolution; only the conversion differs. The `_ns` form skips the conversion to float seconds, so it preserves whatever precision the clock supplied. If the platform's counter only ticks every microsecond, neither function can tell you anything finer.
  • How would you check what resolution the platform's performance counter actually has?
    Call time.get_clock_info("perf_counter"), which returns a namespace with `resolution`, `implementation`, `monotonic` and `adjustable`. The same call works for "monotonic", "time", "process_time" and "thread_time". It is the honest way to answer "can I trust a microsecond number on this machine?" rather than assuming.
  • If you need CPU time rather than elapsed time, which clock do you use?
    time.process_time() for the whole process or time.thread_time() for the calling thread, each with an `_ns` variant. Both exclude time spent asleep or blocked, so a function that waits on I/O shows near-zero CPU time while perf_counter reports the full wall-clock wait. Which one you want depends on whether you are measuring work or latency.

saying these in an interview costs you the question

  • Says perf_counter_ns reads a different, faster clock
  • Thinks a float's precision is fixed regardless of magnitude
  • Claims perf_counter excludes time spent sleeping
  • Believes perf_counter_ns returns seconds as an integer
  • Treats a single perf_counter reading as a timestamp

context