Skip to content

gh-88494: Use QueryPerformanceCounter() for time.monotonic() #116781

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Mar 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions Doc/library/time.rst
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,15 @@ Functions
The reference point of the returned value is undefined, so that only the
difference between the results of two calls is valid.

Clock:

* On Windows, call ``QueryPerformanceCounter()`` and
``QueryPerformanceFrequency()``.
* On macOS, call ``mach_absolute_time()`` and ``mach_timebase_info()``.
* On HP-UX, call ``gethrtime()``.
* Call ``clock_gettime(CLOCK_HIGHRES)`` if available.
* Otherwise, call ``clock_gettime(CLOCK_MONOTONIC)``.

Use :func:`monotonic_ns` to avoid the precision loss caused by the
:class:`float` type.

Expand Down Expand Up @@ -316,6 +325,11 @@ Functions
point of the returned value is undefined, so that only the difference between
the results of two calls is valid.

.. impl-detail::

On CPython, use the same clock than :func:`time.monotonic()` and is a
monotonic clock, i.e. a clock that cannot go backwards.

Use :func:`perf_counter_ns` to avoid the precision loss caused by the
:class:`float` type.

Expand All @@ -324,6 +338,10 @@ Functions
.. versionchanged:: 3.10
On Windows, the function is now system-wide.

.. versionchanged:: 3.13
Use the same clock than :func:`time.monotonic()`.


.. function:: perf_counter_ns() -> int

Similar to :func:`perf_counter`, but return time as nanoseconds.
Expand Down Expand Up @@ -666,6 +684,12 @@ Functions
:class:`struct_time` object is returned, from which the components
of the calendar date may be accessed as attributes.

Clock:

* On Windows, call ``GetSystemTimeAsFileTime()``.
* Call ``clock_gettime(CLOCK_REALTIME)`` if available.
* Otherwise, call ``gettimeofday()``.

Use :func:`time_ns` to avoid the precision loss caused by the :class:`float`
type.

Expand Down
9 changes: 9 additions & 0 deletions Doc/whatsnew/3.13.rst
Original file line number Diff line number Diff line change
Expand Up @@ -552,6 +552,15 @@ sys
This function is not guaranteed to exist in all implementations of Python.
(Contributed by Serhiy Storchaka in :gh:`78573`.)

time
----

* On Windows, :func:`time.monotonic()` now uses the
``QueryPerformanceCounter()`` clock to have a resolution better than 1 us,
instead of the ``GetTickCount64()`` clock which has a resolution of 15.6 ms.
(Contributed by Victor Stinner in :gh:`88494`.)


tkinter
-------

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
On Windows, :func:`time.monotonic()` now uses the ``QueryPerformanceCounter()``
clock to have a resolution better than 1 us, instead of the
``GetTickCount64()`` clock which has a resolution of 15.6 ms. Patch by Victor
Stinner.
211 changes: 73 additions & 138 deletions Python/pytime.c
Original file line number Diff line number Diff line change
Expand Up @@ -1027,9 +1027,76 @@ _PyTime_TimeWithInfo(PyTime_t *t, _Py_clock_info_t *info)
}


#ifdef MS_WINDOWS
static int
py_win_perf_counter_frequency(_PyTimeFraction *base, int raise_exc)
{
LARGE_INTEGER freq;
// Since Windows XP, the function cannot fail.
(void)QueryPerformanceFrequency(&freq);
LONGLONG frequency = freq.QuadPart;

// Since Windows XP, frequency cannot be zero.
assert(frequency >= 1);

Py_BUILD_ASSERT(sizeof(PyTime_t) == sizeof(frequency));
PyTime_t denom = (PyTime_t)frequency;

// Known QueryPerformanceFrequency() values:
//
// * 10,000,000 (10 MHz): 100 ns resolution
// * 3,579,545 Hz (3.6 MHz): 279 ns resolution
if (_PyTimeFraction_Set(base, SEC_TO_NS, denom) < 0) {
if (raise_exc) {
PyErr_SetString(PyExc_RuntimeError,
"invalid QueryPerformanceFrequency");
}
return -1;
}
return 0;
}


// N.B. If raise_exc=0, this may be called without the GIL.
static int
py_get_win_perf_counter(PyTime_t *tp, _Py_clock_info_t *info, int raise_exc)
{
assert(info == NULL || raise_exc);

static _PyTimeFraction base = {0, 0};
if (base.denom == 0) {
if (py_win_perf_counter_frequency(&base, raise_exc) < 0) {
return -1;
}
}

if (info) {
info->implementation = "QueryPerformanceCounter()";
info->resolution = _PyTimeFraction_Resolution(&base);
info->monotonic = 1;
info->adjustable = 0;
}

LARGE_INTEGER now;
QueryPerformanceCounter(&now);
LONGLONG ticksll = now.QuadPart;

/* Make sure that casting LONGLONG to PyTime_t cannot overflow,
both types are signed */
PyTime_t ticks;
static_assert(sizeof(ticksll) <= sizeof(ticks),
"LONGLONG is larger than PyTime_t");
ticks = (PyTime_t)ticksll;

*tp = _PyTimeFraction_Mul(ticks, &base);
return 0;
}
#endif // MS_WINDOWS


#ifdef __APPLE__
static int
py_mach_timebase_info(_PyTimeFraction *base, int raise)
py_mach_timebase_info(_PyTimeFraction *base, int raise_exc)
{
mach_timebase_info_data_t timebase;
// According to the Technical Q&A QA1398, mach_timebase_info() cannot
Expand All @@ -1051,7 +1118,7 @@ py_mach_timebase_info(_PyTimeFraction *base, int raise)
// * (1000000000, 33333335) on PowerPC: ~30 ns
// * (1000000000, 25000000) on PowerPC: 40 ns
if (_PyTimeFraction_Set(base, numer, denom) < 0) {
if (raise) {
if (raise_exc) {
PyErr_SetString(PyExc_RuntimeError,
"invalid mach_timebase_info");
}
Expand All @@ -1069,42 +1136,9 @@ py_get_monotonic_clock(PyTime_t *tp, _Py_clock_info_t *info, int raise_exc)
assert(info == NULL || raise_exc);

#if defined(MS_WINDOWS)
ULONGLONG ticks = GetTickCount64();
static_assert(sizeof(ticks) <= sizeof(PyTime_t),
"ULONGLONG is larger than PyTime_t");
PyTime_t t;
if (ticks <= (ULONGLONG)PyTime_MAX) {
t = (PyTime_t)ticks;
}
else {
// GetTickCount64() maximum is larger than PyTime_t maximum:
// ULONGLONG is unsigned, whereas PyTime_t is signed.
t = PyTime_MAX;
}

int res = pytime_mul(&t, MS_TO_NS);
*tp = t;

if (raise_exc && res < 0) {
pytime_overflow();
if (py_get_win_perf_counter(tp, info, raise_exc) < 0) {
return -1;
}

if (info) {
DWORD timeAdjustment, timeIncrement;
BOOL isTimeAdjustmentDisabled, ok;
info->implementation = "GetTickCount64()";
info->monotonic = 1;
ok = GetSystemTimeAdjustment(&timeAdjustment, &timeIncrement,
&isTimeAdjustmentDisabled);
if (!ok) {
PyErr_SetFromWindowsErr(0);
return -1;
}
info->resolution = timeIncrement * 1e-7;
info->adjustable = 0;
}

#elif defined(__APPLE__)
static _PyTimeFraction base = {0, 0};
if (base.denom == 0) {
Expand Down Expand Up @@ -1190,8 +1224,7 @@ _PyTime_MonotonicUnchecked(void)
{
PyTime_t t;
if (py_get_monotonic_clock(&t, NULL, 0) < 0) {
// If mach_timebase_info(), clock_gettime() or gethrtime() fails:
// silently ignore the failure and return 0.
// Ignore silently the error and return 0.
t = 0;
}
return t;
Expand All @@ -1216,122 +1249,24 @@ _PyTime_MonotonicWithInfo(PyTime_t *tp, _Py_clock_info_t *info)
}


#ifdef MS_WINDOWS
static int
py_win_perf_counter_frequency(_PyTimeFraction *base, int raise)
{
LONGLONG frequency;

LARGE_INTEGER freq;
// Since Windows XP, the function cannot fail.
(void)QueryPerformanceFrequency(&freq);
frequency = freq.QuadPart;

// Since Windows XP, frequency cannot be zero.
assert(frequency >= 1);

Py_BUILD_ASSERT(sizeof(PyTime_t) == sizeof(frequency));
PyTime_t denom = (PyTime_t)frequency;

// Known QueryPerformanceFrequency() values:
//
// * 10,000,000 (10 MHz): 100 ns resolution
// * 3,579,545 Hz (3.6 MHz): 279 ns resolution
if (_PyTimeFraction_Set(base, SEC_TO_NS, denom) < 0) {
if (raise) {
PyErr_SetString(PyExc_RuntimeError,
"invalid QueryPerformanceFrequency");
}
return -1;
}
return 0;
}


// N.B. If raise_exc=0, this may be called without the GIL.
static int
py_get_win_perf_counter(PyTime_t *tp, _Py_clock_info_t *info, int raise_exc)
{
assert(info == NULL || raise_exc);

static _PyTimeFraction base = {0, 0};
if (base.denom == 0) {
if (py_win_perf_counter_frequency(&base, raise_exc) < 0) {
return -1;
}
}

if (info) {
info->implementation = "QueryPerformanceCounter()";
info->resolution = _PyTimeFraction_Resolution(&base);
info->monotonic = 1;
info->adjustable = 0;
}

LARGE_INTEGER now;
QueryPerformanceCounter(&now);
LONGLONG ticksll = now.QuadPart;

/* Make sure that casting LONGLONG to PyTime_t cannot overflow,
both types are signed */
PyTime_t ticks;
static_assert(sizeof(ticksll) <= sizeof(ticks),
"LONGLONG is larger than PyTime_t");
ticks = (PyTime_t)ticksll;

PyTime_t ns = _PyTimeFraction_Mul(ticks, &base);
*tp = ns;
return 0;
}
#endif // MS_WINDOWS


int
_PyTime_PerfCounterWithInfo(PyTime_t *t, _Py_clock_info_t *info)
{
#ifdef MS_WINDOWS
return py_get_win_perf_counter(t, info, 1);
#else
return _PyTime_MonotonicWithInfo(t, info);
#endif
}


PyTime_t
_PyTime_PerfCounterUnchecked(void)
{
PyTime_t t;
int res;
#ifdef MS_WINDOWS
res = py_get_win_perf_counter(&t, NULL, 0);
#else
res = py_get_monotonic_clock(&t, NULL, 0);
#endif
if (res < 0) {
// If py_win_perf_counter_frequency() or py_get_monotonic_clock()
// fails: silently ignore the failure and return 0.
t = 0;
}
return t;
return _PyTime_MonotonicUnchecked();
}


int
PyTime_PerfCounter(PyTime_t *result)
{
int res;
#ifdef MS_WINDOWS
res = py_get_win_perf_counter(result, NULL, 1);
#else
res = py_get_monotonic_clock(result, NULL, 1);
#endif
if (res < 0) {
// If py_win_perf_counter_frequency() or py_get_monotonic_clock()
// fails: silently ignore the failure and return 0.
*result = 0;
return -1;
}
return 0;
return PyTime_Monotonic(result);
}


Expand Down