Skip to content

Reduce DateTimeOffset churn in the cron next-fire-time loop (3.x)#3129

Merged
lahma merged 1 commit into
3.xfrom
cron-datetime-3x
Jun 25, 2026
Merged

Reduce DateTimeOffset churn in the cron next-fire-time loop (3.x)#3129
lahma merged 1 commit into
3.xfrom
cron-datetime-3x

Conversation

@lahma

@lahma lahma commented Jun 23, 2026

Copy link
Copy Markdown
Member

Summary

Port to 3.x of the main-branch DateTimeOffset-churn optimization (#3128), follow-up to #3126.

GetTimeAfter rebuilt a DateTimeOffset after every field even when the field already matched — and since each d.Year / d.Month / d.Day / … access re-decomposes the tick count, an unchanged rebuild cost several decompositions plus a recomposition. In the common steady state most fields are already satisfied, so this was wasted work.

  • The second / minute / hour / day / month / year sections now skip rebuilding the date when the field didn't change. The value they used to rebuild is provably identical to the existing one (same components, milliseconds already stripped at loop entry, same offset), so behavior is unchanged.
  • BitUtil.TrailingZeroCount / PopCount / TryGetMinValueStartingFrom are marked [MethodImpl(AggressiveInlining)]; the dead "start outside [0,63]" guard on the hottest method becomes a Debug.Assert.

Results

BenchmarkDotNet, AMD Ryzen 9 5950X, .NET 10, ShortRun. GetNextValidTimeAfter, before (origin/3.x = #3126) → after:

Expression Before After Δ
0/15 * * * * ? 259 ns 188 ns −27%
0 0,10,…,50 * * * ? 322 ns 234 ns −27%
0 0/5 * * * ? 323 ns 234 ns −28%
0 15 10 * * ? 459 ns 320 ns −30%
0 0-30 9-17 * * ? 352 ns 239 ns −32%
0 0 8-18 ? * MON-FRI 413 ns 280 ns −32%
0 0 12 * * ? 414 ns 271 ns −35%
0 15 10 L * ? 728 ns 470 ns −35%
0 15 10 LW * ? 784 ns 516 ns −34%
0 15 10 ? * 6L 726 ns 467 ns −36%
0 15 10 ? * 6#3 * 736 ns 472 ns −36%

27–36% faster (the win is larger than on main because the monolithic 3.x loop rebuilt the date after every field). Allocation is unchanged (already zero on these paths).

Validation

  • All cron tests pass on net10.0 and net472 (the net472 run exercises the De Bruijn fallback) — 300 cron cases; full unit suite 1401 green; includes the randomized differential test from Speed up cron next-fire-time computation with a bitmask fast path #3126.
  • Each skipped rebuild is guarded by the exact condition under which the value is identical to the existing date, so behavior is unchanged by construction.

Note: 3.x's L/W day-of-month handling is already allocation-free (it computes the day inline, with no per-month SortedSet), so the companion L/W zero-allocation change in #3128 has no 3.x counterpart.

🤖 Generated with Claude Code

https://claude.ai/code/session_01WEQMdnqSWJkTN7KDemLtre

Port of the main-branch optimization (#3128) to 3.x. GetTimeAfter rebuilt a
DateTimeOffset after every field even when the field already matched - and since
each d.Year / d.Month / d.Day / ... access re-decomposes the tick count, an
unchanged rebuild cost several decompositions plus a recomposition. In the common
steady state most fields are already satisfied, so this was pure waste.

- The second / minute / hour / day / month / year sections now skip rebuilding
  the date when the field did not change. The value they used to rebuild is
  provably identical to the existing one (same components, milliseconds already
  stripped at loop entry, same offset), so behavior is unchanged.
- BitUtil.TrailingZeroCount / PopCount / TryGetMinValueStartingFrom are marked
  AggressiveInlining; the dead "start outside [0,63]" guard on the hottest method
  becomes a Debug.Assert (callers always pass an in-range value).

Next-occurrence computation is 27-36% faster (largest on day-of-week, L and
year-constrained expressions, which iterate the loop more). All 1401 unit tests
pass on net10.0 and net472 (the net472 run exercises the De Bruijn fallback),
including the randomized differential test added in #3126.

Note: 3.x's L/W day-of-month handling is already allocation-free (it computes the
day inline, with no per-month SortedSet), so the companion L/W zero-allocation
change in #3128 has no 3.x counterpart.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WEQMdnqSWJkTN7KDemLtre
@lahma lahma merged commit 8857301 into 3.x Jun 25, 2026
16 checks passed
@lahma lahma deleted the cron-datetime-3x branch June 25, 2026 16:35
@sonarqubecloud

Copy link
Copy Markdown

This was referenced Jun 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant