Skip to content

gh-150716: Speed up timedelta construction for integer arguments#150718

Open
gaborbernat wants to merge 1 commit into
python:mainfrom
gaborbernat:opt/timedelta-int-fastpath
Open

gh-150716: Speed up timedelta construction for integer arguments#150718
gaborbernat wants to merge 1 commit into
python:mainfrom
gaborbernat:opt/timedelta-int-fastpath

Conversation

@gaborbernat
Copy link
Copy Markdown
Contributor

@gaborbernat gaborbernat commented Jun 1, 2026

Callers usually build datetime.timedelta from whole numbers of seconds, minutes, or days, yet that common case runs through the same construction path as float and other arguments.

This adds a direct path for whole-number arguments. Other inputs (float, bool, int subclasses, values too large to fit) keep the current behavior, so results and error messages stay the same.

Code that creates timedeltas in bulk gains the most: a pipeline turning a per-row second count into timedelta(seconds=value) over millions of rows, a scheduler handing out timedelta(minutes=n) to thousands of jobs, or retry logic computing timedelta(seconds=2 ** attempt) in a loop.

A pyperf comparison of base versus patched builds (script and full table in a comment below) shows the integer cases run 2.0 to 3.5x faster, geometric mean 2.15x, with the float path flat. A reference check over hundreds of thousands of argument combinations, including overflowing ones, found no differences. This follows the date.today() fast path from gh-88473.

Resolves #150716.

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.
@gaborbernat gaborbernat force-pushed the opt/timedelta-int-fastpath branch from 5bbd195 to 92d33cd Compare June 1, 2026 16:11
@gaborbernat gaborbernat marked this pull request as ready for review June 1, 2026 17:15
@StanFromIreland
Copy link
Copy Markdown
Member

Can you please write a benchmark (ideally using pyperf)?

@gaborbernat
Copy link
Copy Markdown
Contributor Author

gaborbernat commented Jun 1, 2026

Benchmark below, using pyperf. I built two interpreters from this branch's base (main ancestor) and the patch, ran the same script under each, and compared with pyperf compare_to. macOS arm64, non-PGO; the box is not isolated, so treat sub-nanosecond jitter with care, but the 2-3.5x deltas sit far outside the noise.

Script:

import pyperf

SETUP = "from datetime import timedelta"
CASES = {
    "timedelta()": "timedelta()",
    "timedelta(seconds=45)": "timedelta(seconds=45)",
    "timedelta(days=7)": "timedelta(days=7)",
    "timedelta(microseconds=123456)": "timedelta(microseconds=123456)",
    "timedelta(days=2,h=3,m=4,s=5)": "timedelta(days=2, hours=3, minutes=4, seconds=5)",
    "timedelta(days=-1,seconds=-1)": "timedelta(days=-1, seconds=-1)",
    "timedelta(weeks=3)": "timedelta(weeks=3)",
    "timedelta(seconds=1.5)  # float/slow path": "timedelta(seconds=1.5)",
}
runner = pyperf.Runner()
for name, stmt in CASES.items():
    runner.timeit(name=name, stmt=stmt, setup=SETUP)

Results (pyperf compare_to base.json patched.json --table):

+-----------------------------------------------+---------+-----------------------+
| Benchmark                                     | td_base | td_patched            |
+===============================================+=========+=======================+
| timedelta()                                   | 66.0 ns | 18.8 ns: 3.51x faster |
+-----------------------------------------------+---------+-----------------------+
| timedelta(seconds=45)                         | 127 ns  | 62.8 ns: 2.02x faster |
+-----------------------------------------------+---------+-----------------------+
| timedelta(days=7)                             | 151 ns  | 56.8 ns: 2.66x faster |
+-----------------------------------------------+---------+-----------------------+
| timedelta(microseconds=123456)                | 139 ns  | 68.6 ns: 2.03x faster |
+-----------------------------------------------+---------+-----------------------+
| timedelta(days=2,hours=3,minutes=4,seconds=5) | 297 ns  | 126 ns: 2.36x faster  |
+-----------------------------------------------+---------+-----------------------+
| timedelta(days=-1,seconds=-1)                 | 194 ns  | 77.0 ns: 2.52x faster |
+-----------------------------------------------+---------+-----------------------+
| timedelta(weeks=3)                            | 185 ns  | 90.8 ns: 2.04x faster |
+-----------------------------------------------+---------+-----------------------+
| timedelta(seconds=1.5) [float/slow path]      | 151 ns  | 152 ns: 1.01x slower  |
+-----------------------------------------------+---------+-----------------------+
| Geometric mean                                | (ref)   | 2.15x faster          |
+-----------------------------------------------+---------+-----------------------+

The integer cases run 2.0-3.5x faster, geometric mean 2.15x. The float argument keeps the original path and stays flat (1.01x is within noise).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Speed up datetime.timedelta construction for the common all-integer case

2 participants