Skip to content

Add an Int64 fast path to the TimeSpan.From* polyfills - #714

Merged
SimonCropp merged 1 commit into
mainfrom
perf/timespan-from-long-fast-path
Oct 12, 2026
Merged

SimonCropp merged 1 commit into
mainfrom
perf/timespan-from-long-fast-path

Conversation

@SimonCropp

Copy link
Copy Markdown
Owner

The multi-component TimeSpan.From* polyfills (below net9) do all of their arithmetic in decimal, so that components which overflow an Int64 on their own but cancel back into range still give the right answer, as the BCL's 128-bit accumulation does. That costs hundreds of nanoseconds for a call such as TimeSpan.FromDays(1, 2, 3, 4, 5, 6).

Change

All six factories go through one helper. When every component is small enough that its own tick count is at most long.MaxValue / 8, the (at most six) components cannot overflow an Int64 between them, so the ticks are summed directly in long and the result is necessarily in range. The per-component limits are:

Component Fast path up to (either sign)
days 1,334,399 (about 3,653 years)
hours 32,025,597
minutes 1,921,535,841
seconds 115,292,150,460
milliseconds 115,292,150,460,684
microseconds 115,292,150,460,684,697

Anything larger takes the existing decimal path unchanged, which still decides whether the total overflows and throws the same ArgumentOutOfRangeException. The fast path cannot throw and cannot produce a different value: within those limits the long sum is exact.

Measurements

net48, x64, Release, 5,000,000 calls each, two rounds. No allocations before or after.

Call Before After
FromDays(1, 2, 3, 4, 5, 6) 246 / 372 ns 6.5 / 6.3 ns
FromHours(1, 2, 3, 4, 5) 296 / 293 ns 6.4 / 5.5 ns
FromMinutes(1, 2, 3, 4) 266 / 234 ns 8.9 / 5.5 ns
FromSeconds(1, 2, 3) 197 / 208 ns 5.6 / 5.4 ns
FromMilliseconds(1, 2) 105 / 137 ns 5.6 / 7.2 ns
FromMicroseconds(1) 44 / 61 ns 5.6 / 15.6 ns
FromSeconds(1e12, -1e15, 0) (decimal fallback) 129 / 187 ns 164 / 196 ns

The fallback row is the same code as before plus the range checks, and is unchanged within the noise.

Tests

The expected value is computed in the test with exact decimal arithmetic (null when the total is outside the tick range), and every factory that can take the components is compared with it; null must be an ArgumentOutOfRangeException, and any other exception fails the test.

  • a table of fixed cases: zeros, mixed signs, TimeSpan.MaxValue / MinValue truncated to microseconds and one microsecond past them, int/long extremes in every position
  • the tick range boundaries: +/-922337203685477580 microseconds is accepted and gives +/-9223372036854775800 ticks, one more throws; components far out of range that cancel back into range; components that are each in range but overflow together
  • every component on its own at 0, 1, 2^n - 1, 2^n, 2^n + 1 for every n, around every power of ten, long.MinValue/MaxValue, and at long.MaxValue / d / ticksPerUnit and its neighbours for d = 1..16, which covers the fast-path limits (d = 8) and the overflow limits (d = 1) from both sides, in both signs
  • 5,000 random component sets with a random number of significant bits each, through all five multi-component factories
  • FromMicroseconds(long) on its own; below net9 a long argument binds to the BCL's FromMicroseconds(double) on net7 and net8, so the large values are only asserted when a run-time probe shows the exact overload is in use

On net9 and later the same tests run against the BCL.

@SimonCropp SimonCropp modified the milestones: 11.4.4, 11.4.5 Oct 12, 2026
@SimonCropp
SimonCropp merged commit 4563b2b into main Oct 12, 2026
6 of 7 checks passed
@SimonCropp
SimonCropp deleted the perf/timespan-from-long-fast-path branch October 12, 2026 12:12
This was referenced Oct 12, 2026
This was referenced Oct 13, 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