Should time.perf_counter() be consistent across processes in Python on Windows?
In Windows, time.perf_counter
is based on WINAPI QueryPerformanceCounter
. This counter is system wide. For more information, see acquiring high-resolution time stamps.
That said, perf_counter
in Windows returns a value that's relative to the process startup value. Thus it is not a system-wide value. It does this in order to reduce precision loss when converting the integer value to a float
, which has only 15 decimal digits of precision. Using a relative value is uncalled for in most cases, which only need microsecond precision. There should be an optional parameter to query the true QPC counter value, especially for perf_counter_ns
in 3.7+.
Regarding the different initial values returned by perf_counter
in 3.6 vs 3.7, the implementation has changed a bit over time. In 3.6.8, perf_counter
is implemented in Modules/timemodule.c, so the initial value is stored when the time
module is first imported and initialized, which is why you see the first result as 0.000 seconds. In more recent releases it's implemented separately in Python's C API. For example, see "Python/pytime.c" in the latest 3.8 beta release. In this case, by the time Python code calls time.perf_counter()
, the counter has incremented well past the startup value.
Here's an alternative implementation based on ctypes that uses the system-wide QPC value instead of a relative value.
import sysif sys.platform != 'win32': from time import perf_counter try: from time import perf_counter_ns except ImportError: def perf_counter_ns(): """perf_counter_ns() -> int Performance counter for benchmarking as nanoseconds. """ return int(perf_counter() * 10**9)else: import ctypes from ctypes import wintypes kernel32 = ctypes.WinDLL('kernel32', use_last_error=True) kernel32.QueryPerformanceFrequency.argtypes = ( wintypes.PLARGE_INTEGER,) # lpFrequency kernel32.QueryPerformanceCounter.argtypes = ( wintypes.PLARGE_INTEGER,) # lpPerformanceCount _qpc_frequency = wintypes.LARGE_INTEGER() if not kernel32.QueryPerformanceFrequency(ctypes.byref(_qpc_frequency)): raise ctypes.WinError(ctypes.get_last_error()) _qpc_frequency = _qpc_frequency.value def perf_counter_ns(): """perf_counter_ns() -> int Performance counter for benchmarking as nanoseconds. """ count = wintypes.LARGE_INTEGER() if not kernel32.QueryPerformanceCounter(ctypes.byref(count)): raise ctypes.WinError(ctypes.get_last_error()) return (count.value * 10**9) // _qpc_frequency def perf_counter(): """perf_counter() -> float Performance counter for benchmarking. """ count = wintypes.LARGE_INTEGER() if not kernel32.QueryPerformanceCounter(ctypes.byref(count)): raise ctypes.WinError(ctypes.get_last_error()) return count.value / _qpc_frequency
QPC typically has a resolution of 0.1 microseconds. A float
in CPython has 15 decimal digits of precision. So this implementation of perf_counter
is within the QPC resolution for an uptime of about 3 years.