Skip to content

Commit 5bbd195

Browse files
committed
gh-150716: Speed up timedelta construction for integer arguments
timedelta() built its result through PyNumber arithmetic on Python ints plus two checked_divmod() calls, allocating a dozen temporary objects and doing a per-call module-state lookup even for the common all-integer case. Add delta_new_int_fastpath(): accumulate the total microseconds in a 64-bit integer with overflow guards, then normalize with floor division and hand the result to the existing new_delta_ex(). Non-exact-int arguments, 64-bit overflow, and out-of-range day counts fall through to the unchanged object path, so bool, float, int subclasses (including a custom __mul__) and bignums keep byte-identical results and errors.
1 parent 2f8f569 commit 5bbd195

2 files changed

Lines changed: 90 additions & 0 deletions

File tree

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Speed up construction of :class:`datetime.timedelta` when all arguments are
2+
exact :class:`int` objects by accumulating the value with 64-bit integer
3+
arithmetic, falling back to the previous implementation for other argument
4+
types. Patch by Bernát Gábor.

Modules/_datetimemodule.c

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2789,6 +2789,85 @@ accum(const char* tag, PyObject *sofar, PyObject *num, PyObject *factor,
27892789
return NULL;
27902790
}
27912791

2792+
/* Fast path for timedelta() when every supplied argument is an exact int.
2793+
* The total number of microseconds is accumulated in a 64-bit integer and
2794+
* normalized with floor division, exactly mirroring the object path (accum()
2795+
* + checked_divmod()) but without the per-argument allocations and the
2796+
* per-call module-state lookup. *handled is left 0 -- so the caller runs the
2797+
* unchanged object path that reproduces identical results and errors -- for a
2798+
* non-exact-int argument, 64-bit overflow, or a day count that does not fit in
2799+
* C int.
2800+
*/
2801+
static PyObject *
2802+
delta_new_int_fastpath(PyTypeObject *type,
2803+
PyObject *days, PyObject *seconds, PyObject *microseconds,
2804+
PyObject *milliseconds, PyObject *minutes, PyObject *hours,
2805+
PyObject *weeks, int *handled)
2806+
{
2807+
const struct { PyObject *arg; long long factor; } parts[] = {
2808+
{microseconds, 1LL},
2809+
{milliseconds, 1000LL},
2810+
{seconds, 1000000LL},
2811+
{minutes, 60000000LL},
2812+
{hours, 3600000000LL},
2813+
{days, 86400000000LL},
2814+
{weeks, 604800000000LL},
2815+
};
2816+
2817+
*handled = 0;
2818+
long long total_us = 0;
2819+
for (size_t i = 0; i < Py_ARRAY_LENGTH(parts); i++) {
2820+
PyObject *arg = parts[i].arg;
2821+
if (arg == NULL) {
2822+
continue;
2823+
}
2824+
if (!PyLong_CheckExact(arg)) {
2825+
return NULL; /* float / bool / int subclass -> object path */
2826+
}
2827+
int overflow;
2828+
long long value = PyLong_AsLongLongAndOverflow(arg, &overflow);
2829+
if (overflow) {
2830+
return NULL; /* magnitude needs bignum -> object path */
2831+
}
2832+
if (value == -1 && PyErr_Occurred()) {
2833+
*handled = 1; /* genuine error -> propagate as-is */
2834+
return NULL;
2835+
}
2836+
/* value * factor + total_us, bailing to the object path on any
2837+
64-bit overflow. factor is a positive constant, so the bounds
2838+
checks below are portable (no compiler overflow builtins). */
2839+
long long factor = parts[i].factor;
2840+
if (value > LLONG_MAX / factor || value < LLONG_MIN / factor) {
2841+
return NULL; /* 64-bit overflow -> object path (bignum) */
2842+
}
2843+
long long product = value * factor;
2844+
if ((product > 0 && total_us > LLONG_MAX - product) ||
2845+
(product < 0 && total_us < LLONG_MIN - product)) {
2846+
return NULL;
2847+
}
2848+
total_us += product;
2849+
}
2850+
2851+
/* Floor division into (days, seconds, microseconds), matching divmod()
2852+
* with a positive divisor (Python rounds toward negative infinity). */
2853+
long long q = total_us / 1000000, us = total_us % 1000000;
2854+
if (us < 0) {
2855+
us += 1000000;
2856+
q -= 1;
2857+
}
2858+
long long d = q / 86400, s = q % 86400;
2859+
if (s < 0) {
2860+
s += 86400;
2861+
d -= 1;
2862+
}
2863+
if (d < INT_MIN || d > INT_MAX) {
2864+
return NULL; /* let the object path raise the right error */
2865+
}
2866+
2867+
*handled = 1;
2868+
return new_delta_ex((int)d, (int)s, (int)us, 0, type);
2869+
}
2870+
27922871
/*[clinic input]
27932872
@classmethod
27942873
datetime.timedelta.__new__ as delta_new
@@ -2815,6 +2894,13 @@ delta_new_impl(PyTypeObject *type, PyObject *days, PyObject *seconds,
28152894
{
28162895
PyObject *self = NULL;
28172896

2897+
int handled;
2898+
self = delta_new_int_fastpath(type, days, seconds, microseconds,
2899+
milliseconds, minutes, hours, weeks, &handled);
2900+
if (handled) {
2901+
return self;
2902+
}
2903+
28182904
PyObject *current_mod = NULL;
28192905
datetime_state *st = GET_CURRENT_STATE(current_mod);
28202906

0 commit comments

Comments
 (0)