Skip to content

Commit 663addb

Browse files
danmoseleyCopilot
andauthored
Simplify TryParseFormatO fractional-seconds handling (#129005)
### Simplify `TryParseFormatO` fractional-seconds handling The `"O"` (ISO 8601 round-trip) format always has exactly 7 fractional-second digits, which matches `DateTime`'s tick precision (`TimeSpan.TicksPerSecond == 10_000_000`). The integer formed by those seven digits is therefore *exactly* the sub-second tick count, so we can pass it straight to `TryAddTicks` instead of routing it through a `double` divide / multiply / `Math.Round` round-trip. The change is bit-exact: every one of the 10M possible 7-digit values produces the same tick count as before (verified by brute-force). ### Tests Existing `System.Tests.DateTimeTests` (1378 tests) and `System.Tests.DateTimeOffsetTests` (737 tests, 2 platform-skipped) pass against a CoreLib built with the change. ### Microbenchmark Side benefit: a small (~6%) speedup on `Perf_DateTime.ParseO` on R2R'd CoreLib. Benchmarks used (unchanged, from [`dotnet/performance`](https://github.com/dotnet/performance)): - [`Perf_DateTime.ParseO`](https://github.com/dotnet/performance/blob/643b4e7022538b58bfc274312730a6116dc91c55/src/benchmarks/micro/libraries/System.Runtime/Perf.DateTime.cs#L52) — target - [`Perf_DateTime.ParseR`](https://github.com/dotnet/performance/blob/643b4e7022538b58bfc274312730a6116dc91c55/src/benchmarks/micro/libraries/System.Runtime/Perf.DateTime.cs#L49) — unchanged control Run with BenchmarkDotNet `Job.Default`, same-process A/B (both `--coreRun`s passed in one invocation so process-startup, ASLR, and thermal effects don't bias the comparison), affinity-pinned to a single P-core: ``` dotnet run -c Release -f net11.0 --project src/benchmarks/micro/MicroBenchmarks.csproj -- \ --filter "System.Tests.Perf_DateTime.ParseO" "System.Tests.Perf_DateTime.ParseR" \ --coreRun <baseline-testhost>\CoreRun.exe <patched-testhost>\CoreRun.exe \ --affinity 1 ``` Environment: Windows 11, Intel Core i9-14900K (P-core 0 pinned), .NET 11.0 R2R'd `System.Private.CoreLib` (Release), BenchmarkDotNet v0.16.0-nightly. | Method | Job | Mean | Error | StdDev | Ratio | RatioSD | |------- |----------- |---------:|---------:|---------:|------:|--------:| | ParseR | baseline | 11.42 ns | 0.168 ns | 0.149 ns | 1.00 | 0.00 | | ParseR | patched | 11.41 ns | 0.163 ns | 0.144 ns | 1.00 | 0.02 | | | | | | | | | | ParseO | baseline | 11.87 ns | 0.198 ns | 0.175 ns | 1.00 | 0.00 | | ParseO | patched | 11.14 ns | 0.125 ns | 0.111 ns | 0.94 | 0.02 | `ParseR` (control) is identical between the two binaries, confirming the harness is stable; `ParseO` improves ~6% (~0.73 ns/call). > [!NOTE] > This PR was prepared with the assistance of GitHub Copilot. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent e99ab10 commit 663addb

1 file changed

Lines changed: 8 additions & 4 deletions

File tree

src/libraries/System.Private.CoreLib/src/System/Globalization/DateTimeParse.cs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5171,7 +5171,11 @@ private static bool TryParseFormatO(ReadOnlySpan<char> source, scoped ref DateTi
51715171
second = (int)(s1 * 10 + s2);
51725172
}
51735173

5174-
double fraction;
5174+
// The "O" format always has exactly 7 fractional-second digits, which is the same precision
5175+
// as DateTime's ticks (TimeSpan.TicksPerSecond == 10_000_000), so the integer value formed
5176+
// by the seven digits is exactly the sub-second tick count. Compute it directly instead of
5177+
// going through a double divide/multiply/Math.Round round-trip.
5178+
int fractionTicks;
51755179
{
51765180
uint f1 = (uint)(source[20] - '0');
51775181
uint f2 = (uint)(source[21] - '0');
@@ -5187,12 +5191,12 @@ private static bool TryParseFormatO(ReadOnlySpan<char> source, scoped ref DateTi
51875191
return false;
51885192
}
51895193

5190-
fraction = (f1 * 1000000 + f2 * 100000 + f3 * 10000 + f4 * 1000 + f5 * 100 + f6 * 10 + f7) / 10000000.0;
5194+
fractionTicks = (int)(f1 * 1000000 + f2 * 100000 + f3 * 10000 + f4 * 1000 + f5 * 100 + f6 * 10 + f7);
51915195
}
51925196

51935197
// Per ISO 8601, 24:00:00 represents the end of a calendar day
51945198
// (the same instant as the next day's 00:00:00), but only when minute, second, and fraction are all zero
5195-
if (hour == 24 && (minute != 0 || second != 0 || fraction != 0))
5199+
if (hour == 24 && (minute != 0 || second != 0 || fractionTicks != 0))
51965200
{
51975201
result.SetBadDateTimeFailure();
51985202
return false;
@@ -5204,7 +5208,7 @@ private static bool TryParseFormatO(ReadOnlySpan<char> source, scoped ref DateTi
52045208
return false;
52055209
}
52065210

5207-
if (!dateTime.TryAddTicks((long)Math.Round(fraction * TimeSpan.TicksPerSecond), out result.parsedDate))
5211+
if (!dateTime.TryAddTicks(fractionTicks, out result.parsedDate))
52085212
{
52095213
result.SetBadDateTimeFailure();
52105214
return false;

0 commit comments

Comments
 (0)