Uh oh!
There was an error while loading. Please reload this page.
Refactor some DateTime and TimeSpan formatting/parsing methods - #101640
Conversation
Tagging subscribers to this area: @dotnet/area-system-globalization |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
danmoseley
commented
Apr 28, 2024
Are these sufficiently covered in dotnet/performance? Might be worth running appropriate benchmarks there. |
| Debug.Assert(totalDigitsCount - MaxFractionDigits <= MaxFractionDigits); | ||
| uint power = (uint)Pow10UpToMaxFractionDigits(totalDigitsCount - MaxFractionDigits); | ||
| _num = (int)(((uint)_num + power / 2) / power); |
There was a problem hiding this comment.
I am not sure if this dividing by 2 and dividing by power will be more performant than just calling Math.Round?
@tannergooding any opinion on that?
There was a problem hiding this comment.
constintMaxFractionDigits=7;[Params(123456789)]publicintNum{get;set;}[Params(10)]publicintTotalDigitsCount{get;set;}internalstaticintPow10UpToMaxFractionDigits(intpow){ReadOnlySpan<int>powersOfTen=[1,10,100,1000,10000,100000,1000000,10000000,];Debug.Assert(powersOfTen.Length==MaxFractionDigits+1);returnpowersOfTen[pow];}[Benchmark]publicintMathRound()=>(int)Math.Round((double)Num/Pow10UpToMaxFractionDigits(TotalDigitsCount-MaxFractionDigits),MidpointRounding.AwayFromZero);[Benchmark]publicintIntegerRound(){uintpower=(uint)Pow10UpToMaxFractionDigits(TotalDigitsCount-MaxFractionDigits);return(int)(((uint)Num+power/2)/power);}| Method | Num | TotalDigitsCount | Mean | Error | StdDev |
|---|---|---|---|---|---|
| MathRound | 123456789 | 10 | 0.8892 ns | 0.0068 ns | 0.0061 ns |
| IntegerRound | 123456789 | 10 | 1.2103 ns | 0.0044 ns | 0.0034 ns |
Interesting...I suppose floating point math could be much slower than integer.
There was a problem hiding this comment.
I suggest reverting the code back to use Math.Round. It is even more readable and clear the intention of what the code does.
There was a problem hiding this comment.
Results on arm64 with the span bounds check removed:
BenchmarkDotNet v0.13.12, macOS Sonoma 14.4.1 (23E224) [Darwin 23.4.0]
Apple M2, 1 CPU, 8 logical and 8 physical cores
.NET SDK 8.0.204
[Host] : .NET 8.0.4 (8.0.424.16909), Arm64 RyuJIT AdvSIMD
DefaultJob : .NET 8.0.4 (8.0.424.16909), Arm64 RyuJIT AdvSIMD
| Method | Num | TotalDigitsCount | Mean | Error | StdDev |
|---|---|---|---|---|---|
| MathRound | 123456789 | 10 | 0.4477 ns | 0.0105 ns | 0.0093 ns |
| IntegerRound | 123456789 | 10 | 0.1372 ns | 0.0037 ns | 0.0032 ns |
There was a problem hiding this comment.
Results on arm64 with the span bounds check removed:
Can you provide a public API benchmark that shows any noticeable perf difference? IMO, It doesn't make sense to benchmark internals.
There was a problem hiding this comment.
@EgorBo Not sure about real-world benchmarks, but with the suggested change code size (on X64) is 172 bytes vs is 271 bytes.
There was a problem hiding this comment.
@lilinus is it possible you run your perf tests again mentioned in #101640 (comment) with @xtqqczze suggestion to get some idea about the perf of this change? Thanks!
I am talking about the change
uintpower=(uint)Pow10UpToMaxFractionDigits(totalDigitsCount-MaxFractionDigits);_num=(int)(((uint)_num+power/2)/power);There was a problem hiding this comment.
Seems like uint division is slightly faster. I assume you want me to push back to that change so I will do it.
| Method | Job | Toolchain | arg | Mean | Error | StdDev | Median | Min | Max | Ratio | Allocated | Alloc Ratio |
|--------------- |----------- |---------------|------------------------- |---------:|--------:|--------:|---------:|---------:|---------:|------:|----------:|------------:|
| TimeSpan_Parse | Job-PGUVPC | main | 00:00:00.000000001234567 | 215.0 ns | 0.90 ns | 0.84 ns | 215.0 ns | 213.3 ns | 216.7 ns | 1.00 | - | NA |
| TimeSpan_Parse | Job-AKPZDD | uint division | 00:00:00.000000001234567 | 154.5 ns | 0.43 ns | 0.36 ns | 154.4 ns | 154.1 ns | 155.4 ns | 0.72 | - | NA |
| TimeSpan_Parse | Job-RERBPQ | Mth.Round | 00:00:00.000000001234567 | 155.8 ns | 0.40 ns | 0.35 ns | 155.7 ns | 155.1 ns | 156.4 ns | 0.72 | - | NA |
| TimeSpan_Parse | Job-PGUVPC | main | 00:00:00.00001234567 | 156.1 ns | 0.48 ns | 0.43 ns | 155.8 ns | 155.6 ns | 157.0 ns | 1.00 | - | NA |
| TimeSpan_Parse | Job-AKPZDD | uint division | 00:00:00.00001234567 | 147.1 ns | 0.72 ns | 0.68 ns | 146.7 ns | 146.3 ns | 148.4 ns | 0.94 | - | NA |
| TimeSpan_Parse | Job-RERBPQ | Math.Round | 00:00:00.00001234567 | 151.3 ns | 0.53 ns | 0.50 ns | 151.4 ns | 150.2 ns | 151.9 ns | 0.97 | - | NA |
Benchmark source:
public static IEnumerable<object> Values()
{
yield return "00:00:00.00001234567";
yield return "00:00:00.000000001234567";
}
[Benchmark, ArgumentsSource(nameof(Values))]
public TimeSpan TimeSpan_Parse(string arg) => TimeSpan.Parse(arg, CultureInfo.InvariantCulture);
There was a problem hiding this comment.
@lilinus looks you already did :-) I am fine with that as tt provides us with a smaller code size and a slightly faster performance as well.
One last ask, please add a comment above that line telling what is does to be easier for anyone reading the code understand what the code is doing.
@xtqqczze thanks for the suggestion!
Uh oh!
There was an error while loading. Please reload this page.
tarekgh
commented
May 2, 2024
@lilinus could you please merge the latest change from main to this PR to be able to proceed? |
lilinus
commented
May 2, 2024
@tarekgh, done. Thanks for the feedback etc. I also tried benchmarking the changes but had trouble with antivirus stopping the runtime. |
tarekgh
commented
May 2, 2024
Is it possible you can temporary disable the antivirus? Or add exclusion to the runtime and test folders to have the antivirus ignore them? |
lilinus
commented
May 2, 2024
Managed to get it working. Benchmarked some DateTime and TimeSpan parsing. Results are looking good IMO. Link to benchmarks: here |
Uh oh!
There was an error while loading. Please reload this page.
…TimeSpanParse.cs Co-authored-by: xtqqczze <45661989+xtqqczze@users.noreply.github.com>
…t#101640) * Refactor some DateTime and TimeSpan formatting/parsing methods * Fix assertion in TimeSpanParse.Pow10 * Don't use Unsafe in TimeSpanParse.Pow10 * Revert changes to TimeSpanParse.Pow10 * Revert "Revert changes to TimeSpanParse.Pow10" This reverts commit 267d5e8. * Change method name to Pow10UpToMaxFractionDigits * Fix TimeSpanParse.TimeSpanToken.NormalizeAndValidateFraction * Address feedback in TimeSpanParse * Change from Math.Round to uint divison in TimeSpanParse.NormalizeAndValidateFraction * Comment for rounding division in TimeSpanParse.NormalizeAndValidateFraction * Update src/libraries/System.Private.CoreLib/src/System/Globalization/TimeSpanParse.cs Co-authored-by: xtqqczze <45661989+xtqqczze@users.noreply.github.com> --------- Co-authored-by: xtqqczze <45661989+xtqqczze@users.noreply.github.com>
…t#101640) * Refactor some DateTime and TimeSpan formatting/parsing methods * Fix assertion in TimeSpanParse.Pow10 * Don't use Unsafe in TimeSpanParse.Pow10 * Revert changes to TimeSpanParse.Pow10 * Revert "Revert changes to TimeSpanParse.Pow10" This reverts commit 267d5e8. * Change method name to Pow10UpToMaxFractionDigits * Fix TimeSpanParse.TimeSpanToken.NormalizeAndValidateFraction * Address feedback in TimeSpanParse * Change from Math.Round to uint divison in TimeSpanParse.NormalizeAndValidateFraction * Comment for rounding division in TimeSpanParse.NormalizeAndValidateFraction * Update src/libraries/System.Private.CoreLib/src/System/Globalization/TimeSpanParse.cs Co-authored-by: xtqqczze <45661989+xtqqczze@users.noreply.github.com> --------- Co-authored-by: xtqqczze <45661989+xtqqczze@users.noreply.github.com>
FormattingHelpers.CountDigits.TimeSpanParse.Pow10inDateTimeFormat.longtoint.