Skip to content

Commit

Permalink
pythongh-112567: Add _PyTimeFraction C API (python#112568)
Browse files Browse the repository at this point in the history
Use a fraction internally in the _PyTime API to reduce the risk of
integer overflow: simplify the fraction using Greatest Common
Divisor (GCD). The fraction API is used by time functions:
perf_counter(), monotonic() and process_time().

For example, QueryPerformanceFrequency() usually returns 10 MHz on
Windows 10 and newer. The fraction SEC_TO_NS / frequency =
1_000_000_000 / 10_000_000 can be simplified to 100 / 1.

* Add _PyTimeFraction type.
* Add functions:

  * _PyTimeFraction_Set()
  * _PyTimeFraction_Mul()
  * _PyTimeFraction_Resolution()

* No longer check "numer * denom <= _PyTime_MAX" in
  _PyTimeFraction_Set(). _PyTimeFraction_Mul() uses _PyTime_Mul()
  which handles integer overflow.
  • Loading branch information
vstinner authored Dec 1, 2023
1 parent 05a370a commit 5c5022b
Show file tree
Hide file tree
Showing 3 changed files with 130 additions and 107 deletions.
33 changes: 26 additions & 7 deletions Include/internal/pycore_time.h
Original file line number Diff line number Diff line change
Expand Up @@ -253,13 +253,6 @@ PyAPI_FUNC(void) _PyTime_AsTimespec_clamp(_PyTime_t t, struct timespec *ts);
// Compute t1 + t2. Clamp to [_PyTime_MIN; _PyTime_MAX] on overflow.
extern _PyTime_t _PyTime_Add(_PyTime_t t1, _PyTime_t t2);

// Compute ticks * mul / div.
// Clamp to [_PyTime_MIN; _PyTime_MAX] on overflow.
// The caller must ensure that ((div - 1) * mul) cannot overflow.
extern _PyTime_t _PyTime_MulDiv(_PyTime_t ticks,
_PyTime_t mul,
_PyTime_t div);

// Structure used by time.get_clock_info()
typedef struct {
const char *implementation;
Expand Down Expand Up @@ -355,6 +348,32 @@ PyAPI_FUNC(_PyTime_t) _PyDeadline_Init(_PyTime_t timeout);
PyAPI_FUNC(_PyTime_t) _PyDeadline_Get(_PyTime_t deadline);


// --- _PyTimeFraction -------------------------------------------------------

typedef struct {
_PyTime_t numer;
_PyTime_t denom;
} _PyTimeFraction;

// Set a fraction.
// Return 0 on success.
// Return -1 if the fraction is invalid.
extern int _PyTimeFraction_Set(
_PyTimeFraction *frac,
_PyTime_t numer,
_PyTime_t denom);

// Compute ticks * frac.numer / frac.denom.
// Clamp to [_PyTime_MIN; _PyTime_MAX] on overflow.
extern _PyTime_t _PyTimeFraction_Mul(
_PyTime_t ticks,
const _PyTimeFraction *frac);

// Compute a clock resolution: frac.numer / frac.denom / 1e9.
extern double _PyTimeFraction_Resolution(
const _PyTimeFraction *frac);


#ifdef __cplusplus
}
#endif
Expand Down
54 changes: 19 additions & 35 deletions Modules/timemodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -69,25 +69,6 @@ module time
/*[clinic end generated code: output=da39a3ee5e6b4b0d input=a668a08771581f36]*/


#if defined(HAVE_TIMES) || defined(HAVE_CLOCK)
static int
check_ticks_per_second(long tps, const char *context)
{
/* Effectively, check that _PyTime_MulDiv(t, SEC_TO_NS, tps)
cannot overflow. */
if (tps >= 0 && (_PyTime_t)tps > _PyTime_MAX / SEC_TO_NS) {
PyErr_Format(PyExc_OverflowError, "%s is too large", context);
return -1;
}
if (tps < 1) {
PyErr_Format(PyExc_RuntimeError, "invalid %s", context);
return -1;
}
return 0;
}
#endif /* HAVE_TIMES || HAVE_CLOCK */


/* Forward declarations */
static int pysleep(_PyTime_t timeout);

Expand All @@ -96,11 +77,11 @@ typedef struct {
PyTypeObject *struct_time_type;
#ifdef HAVE_TIMES
// times() clock frequency in hertz
long ticks_per_second;
_PyTimeFraction times_base;
#endif
#ifdef HAVE_CLOCK
// clock() frequency in hertz
long clocks_per_second;
_PyTimeFraction clock_base;
#endif
} time_module_state;

Expand Down Expand Up @@ -174,10 +155,11 @@ Return the current time in nanoseconds since the Epoch.");
static int
py_clock(time_module_state *state, _PyTime_t *tp, _Py_clock_info_t *info)
{
long clocks_per_second = state->clocks_per_second;
_PyTimeFraction *base = &state->clock_base;

if (info) {
info->implementation = "clock()";
info->resolution = 1.0 / (double)clocks_per_second;
info->resolution = _PyTimeFraction_Resolution(base);
info->monotonic = 1;
info->adjustable = 0;
}
Expand All @@ -189,7 +171,7 @@ py_clock(time_module_state *state, _PyTime_t *tp, _Py_clock_info_t *info)
"or its value cannot be represented");
return -1;
}
_PyTime_t ns = _PyTime_MulDiv(ticks, SEC_TO_NS, clocks_per_second);
_PyTime_t ns = _PyTimeFraction_Mul(ticks, base);
*tp = _PyTime_FromNanoseconds(ns);
return 0;
}
Expand Down Expand Up @@ -1257,7 +1239,7 @@ static int
process_time_times(time_module_state *state, _PyTime_t *tp,
_Py_clock_info_t *info)
{
long ticks_per_second = state->ticks_per_second;
_PyTimeFraction *base = &state->times_base;

struct tms process;
if (times(&process) == (clock_t)-1) {
Expand All @@ -1266,14 +1248,14 @@ process_time_times(time_module_state *state, _PyTime_t *tp,

if (info) {
info->implementation = "times()";
info->resolution = _PyTimeFraction_Resolution(base);
info->monotonic = 1;
info->adjustable = 0;
info->resolution = 1.0 / (double)ticks_per_second;
}

_PyTime_t ns;
ns = _PyTime_MulDiv(process.tms_utime, SEC_TO_NS, ticks_per_second);
ns += _PyTime_MulDiv(process.tms_stime, SEC_TO_NS, ticks_per_second);
ns = _PyTimeFraction_Mul(process.tms_utime, base);
ns += _PyTimeFraction_Mul(process.tms_stime, base);
*tp = _PyTime_FromNanoseconds(ns);
return 1;
}
Expand Down Expand Up @@ -1395,8 +1377,7 @@ py_process_time(time_module_state *state, _PyTime_t *tp,
// times() failed, ignore failure
#endif

/* clock */
/* Currently, Python 3 requires clock() to build: see issue #22624 */
/* clock(). Python 3 requires clock() to build (see gh-66814) */
return py_clock(state, tp, info);
#endif
}
Expand Down Expand Up @@ -2110,20 +2091,23 @@ time_exec(PyObject *module)
#endif

#ifdef HAVE_TIMES
if (_Py_GetTicksPerSecond(&state->ticks_per_second) < 0) {
long ticks_per_second;
if (_Py_GetTicksPerSecond(&ticks_per_second) < 0) {
PyErr_SetString(PyExc_RuntimeError,
"cannot read ticks_per_second");
return -1;
}

if (check_ticks_per_second(state->ticks_per_second, "_SC_CLK_TCK") < 0) {
if (_PyTimeFraction_Set(&state->times_base, SEC_TO_NS,
ticks_per_second) < 0) {
PyErr_Format(PyExc_OverflowError, "ticks_per_second is too large");
return -1;
}
#endif

#ifdef HAVE_CLOCK
state->clocks_per_second = CLOCKS_PER_SEC;
if (check_ticks_per_second(state->clocks_per_second, "CLOCKS_PER_SEC") < 0) {
if (_PyTimeFraction_Set(&state->clock_base, SEC_TO_NS,
CLOCKS_PER_SEC) < 0) {
PyErr_Format(PyExc_OverflowError, "CLOCKS_PER_SEC is too large");
return -1;
}
#endif
Expand Down
Loading

0 comments on commit 5c5022b

Please sign in to comment.