Should time.perf_counter() be consistent across processes in Python on Windows? Should time.perf_counter() be consistent across processes in Python on Windows? windows windows

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.