Skip to content

datetime.replace() is very slow #112919

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

Closed
eltoder opened this issue Dec 10, 2023 · 1 comment
Closed

datetime.replace() is very slow #112919

eltoder opened this issue Dec 10, 2023 · 1 comment
Labels
3.13 bugs and security fixes performance Performance or resource usage type-feature A feature request or enhancement

Comments

@eltoder
Copy link
Contributor

eltoder commented Dec 10, 2023

Bug report

Bug description:

datetime.replace() is very slow when called with keyword arguments. For example, with python 3.13 from main on my laptop

$ python -m timeit -s "from datetime import datetime; dt = datetime.now()" "dt.replace(microsecond=0)"
500000 loops, best of 5: 501 nsec per loop

For comparison,

$ python -m timeit -s "from datetime import datetime" "datetime.now()"
2000000 loops, best of 5: 150 nsec per loop
$ python -m timeit -s "from datetime import datetime" "datetime(2020, 1, 1, 12, 34, 56, 123456)"
2000000 loops, best of 5: 119 nsec per loop

So calling replace() is over 4x slower than constructing a new datetime from components and over 3x slower than now(), which makes a system call.

CPython versions tested on:

3.9, 3.10, 3.13

Operating systems tested on:

Linux

Linked PRs

@eltoder eltoder added the type-bug An unexpected behavior, bug, or error label Dec 10, 2023
eltoder added a commit to eltoder/cpython that referenced this issue Dec 10, 2023
Use argument clinic and call new_* functions directly. This speeds up
these functions 6x to 7.5x when calling with keyword arguments.

Before:
$ python -m timeit -s "from datetime import datetime; dt = datetime.now()" "dt.replace(microsecond=0)"
500000 loops, best of 5: 501 nsec per loop

$ python -m timeit -s "from datetime import date; d = date.today()" "d.replace(day=1)"
1000000 loops, best of 5: 273 nsec per loop

$ python -m timeit -s "from datetime import time; t = time()" "t.replace(microsecond=0)"
1000000 loops, best of 5: 338 nsec per loop

After:
$ python -m timeit -s "from datetime import datetime; dt = datetime.now()" "dt.replace(microsecond=0)"
5000000 loops, best of 5: 65.9 nsec per loop

$ python -m timeit -s "from datetime import date; d = date.today()" "d.replace(day=1)"
5000000 loops, best of 5: 45.6 nsec per loop

$ python -m timeit -s "from datetime import time; t = time()" "t.replace(microsecond=0)"
5000000 loops, best of 5: 51 nsec per loop
@JelleZijlstra JelleZijlstra added the performance Performance or resource usage label Dec 10, 2023
eltoder added a commit to eltoder/cpython that referenced this issue Dec 10, 2023
Use argument clinic and call new_* functions directly. This speeds up
these functions 6x to 7.5x when calling with keyword arguments.

Before:
$ python -m timeit -s "from datetime import datetime; dt = datetime.now()" "dt.replace(microsecond=0)"
500000 loops, best of 5: 501 nsec per loop

$ python -m timeit -s "from datetime import date; d = date.today()" "d.replace(day=1)"
1000000 loops, best of 5: 273 nsec per loop

$ python -m timeit -s "from datetime import time; t = time()" "t.replace(microsecond=0)"
1000000 loops, best of 5: 338 nsec per loop

After:
$ python -m timeit -s "from datetime import datetime; dt = datetime.now()" "dt.replace(microsecond=0)"
5000000 loops, best of 5: 65.9 nsec per loop

$ python -m timeit -s "from datetime import date; d = date.today()" "d.replace(day=1)"
5000000 loops, best of 5: 45.6 nsec per loop

$ python -m timeit -s "from datetime import time; t = time()" "t.replace(microsecond=0)"
5000000 loops, best of 5: 51 nsec per loop
eltoder added a commit to eltoder/cpython that referenced this issue Dec 10, 2023
Use argument clinic and call new_* functions directly. This speeds up
these functions 6x to 7.5x when calling with keyword arguments.

Before:
$ python -m timeit -s "from datetime import datetime; dt = datetime.now()" "dt.replace(microsecond=0)"
500000 loops, best of 5: 501 nsec per loop

$ python -m timeit -s "from datetime import date; d = date.today()" "d.replace(day=1)"
1000000 loops, best of 5: 273 nsec per loop

$ python -m timeit -s "from datetime import time; t = time()" "t.replace(microsecond=0)"
1000000 loops, best of 5: 338 nsec per loop

After:
$ python -m timeit -s "from datetime import datetime; dt = datetime.now()" "dt.replace(microsecond=0)"
5000000 loops, best of 5: 65.9 nsec per loop

$ python -m timeit -s "from datetime import date; d = date.today()" "d.replace(day=1)"
5000000 loops, best of 5: 45.6 nsec per loop

$ python -m timeit -s "from datetime import time; t = time()" "t.replace(microsecond=0)"
5000000 loops, best of 5: 51 nsec per loop
eltoder added a commit to eltoder/cpython that referenced this issue Dec 10, 2023
Use argument clinic and call new_* functions directly. This speeds up
these functions 6x to 7.5x when calling with keyword arguments.

Before:
$ python -m timeit -s "from datetime import datetime; dt = datetime.now()" "dt.replace(microsecond=0)"
500000 loops, best of 5: 501 nsec per loop

$ python -m timeit -s "from datetime import date; d = date.today()" "d.replace(day=1)"
1000000 loops, best of 5: 273 nsec per loop

$ python -m timeit -s "from datetime import time; t = time()" "t.replace(microsecond=0)"
1000000 loops, best of 5: 338 nsec per loop

After:
$ python -m timeit -s "from datetime import datetime; dt = datetime.now()" "dt.replace(microsecond=0)"
5000000 loops, best of 5: 65.9 nsec per loop

$ python -m timeit -s "from datetime import date; d = date.today()" "d.replace(day=1)"
5000000 loops, best of 5: 45.6 nsec per loop

$ python -m timeit -s "from datetime import time; t = time()" "t.replace(microsecond=0)"
5000000 loops, best of 5: 51 nsec per loop
eltoder added a commit to eltoder/cpython that referenced this issue Dec 10, 2023
Use argument clinic and call new_* functions directly. This speeds up
these functions 6x to 7.5x when calling with keyword arguments.

Before:
$ python -m timeit -s "from datetime import datetime; dt = datetime.now()" "dt.replace(microsecond=0)"
500000 loops, best of 5: 501 nsec per loop

$ python -m timeit -s "from datetime import date; d = date.today()" "d.replace(day=1)"
1000000 loops, best of 5: 273 nsec per loop

$ python -m timeit -s "from datetime import time; t = time()" "t.replace(microsecond=0)"
1000000 loops, best of 5: 338 nsec per loop

After:
$ python -m timeit -s "from datetime import datetime; dt = datetime.now()" "dt.replace(microsecond=0)"
5000000 loops, best of 5: 65.9 nsec per loop

$ python -m timeit -s "from datetime import date; d = date.today()" "d.replace(day=1)"
5000000 loops, best of 5: 45.6 nsec per loop

$ python -m timeit -s "from datetime import time; t = time()" "t.replace(microsecond=0)"
5000000 loops, best of 5: 51 nsec per loop
eltoder added a commit to eltoder/cpython that referenced this issue Dec 10, 2023
Use argument clinic and call new_* functions directly. This speeds up
these functions 6x to 7.5x when calling with keyword arguments.

Before:
$ python -m timeit -s "from datetime import datetime; dt = datetime.now()" "dt.replace(microsecond=0)"
500000 loops, best of 5: 501 nsec per loop

$ python -m timeit -s "from datetime import date; d = date.today()" "d.replace(day=1)"
1000000 loops, best of 5: 273 nsec per loop

$ python -m timeit -s "from datetime import time; t = time()" "t.replace(microsecond=0)"
1000000 loops, best of 5: 338 nsec per loop

After:
$ python -m timeit -s "from datetime import datetime; dt = datetime.now()" "dt.replace(microsecond=0)"
5000000 loops, best of 5: 65.9 nsec per loop

$ python -m timeit -s "from datetime import date; d = date.today()" "d.replace(day=1)"
5000000 loops, best of 5: 45.6 nsec per loop

$ python -m timeit -s "from datetime import time; t = time()" "t.replace(microsecond=0)"
5000000 loops, best of 5: 51 nsec per loop
serhiy-storchaka pushed a commit that referenced this issue Jan 30, 2024
Use argument clinic and call new_* functions directly. This speeds up
these functions 6x to 7.5x when calling with keyword arguments.
@serhiy-storchaka serhiy-storchaka added type-feature A feature request or enhancement 3.13 bugs and security fixes and removed type-bug An unexpected behavior, bug, or error labels Jan 30, 2024
@serhiy-storchaka
Copy link
Member

Thank you for your contribution @eltoder.

I reclassified it as a new feature. It was not that replace() methods were too slow, it was that constructors were too fast.

aisk pushed a commit to aisk/cpython that referenced this issue Feb 11, 2024
…-112921)

Use argument clinic and call new_* functions directly. This speeds up
these functions 6x to 7.5x when calling with keyword arguments.
eltoder added a commit to eltoder/cpython that referenced this issue Feb 12, 2024
…-112921)

Use argument clinic and call new_* functions directly. This speeds up
these functions 6x to 7.5x when calling with keyword arguments.

(cherry picked from commit 1f515e8)
eltoder added a commit to eltoder/cpython that referenced this issue Feb 12, 2024
…ythonGH-112921)

Use argument clinic and call new_* functions directly. This speeds up
these functions 6x to 7.5x when calling with keyword arguments.
(cherry picked from commit 1f515e8)

Co-authored-by: Eugene Toder <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
3.13 bugs and security fixes performance Performance or resource usage type-feature A feature request or enhancement
Projects
None yet
Development

No branches or pull requests

3 participants