-
Notifications
You must be signed in to change notification settings - Fork 4.8k
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
Refactor some DateTime and TimeSpan formatting/parsing methods #101640
Conversation
Tagging subscribers to this area: @dotnet/area-system-globalization |
src/libraries/System.Private.CoreLib/src/System/Globalization/TimeSpanParse.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Private.CoreLib/src/System/Globalization/TimeSpanParse.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Private.CoreLib/src/System/Globalization/TimeSpanParse.cs
Outdated
Show resolved
Hide resolved
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
const int MaxFractionDigits = 7;
[Params(123456789)]
public int Num { get; set; }
[Params(10)]
public int TotalDigitsCount { get; set; }
internal static int Pow10UpToMaxFractionDigits(int pow)
{
ReadOnlySpan<int> powersOfTen =
[
1,
10,
100,
1000,
10000,
100000,
1000000,
10000000,
];
Debug.Assert(powersOfTen.Length == MaxFractionDigits + 1);
return powersOfTen[pow];
}
[Benchmark]
public int MathRound() => (int)Math.Round((double)Num / Pow10UpToMaxFractionDigits(TotalDigitsCount - MaxFractionDigits), MidpointRounding.AwayFromZero);
[Benchmark]
public int IntegerRound()
{
uint power = (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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@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
uint power = (uint)Pow10UpToMaxFractionDigits(totalDigitsCount - MaxFractionDigits);
_num = (int)(((uint)_num + power / 2) / power);
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@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!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Comment added 👍
src/libraries/System.Private.CoreLib/src/System/Globalization/TimeSpanParse.cs
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@lilinus thanks for helping with this change.
@lilinus could you please merge the latest change from main to this PR to be able to proceed? |
@tarekgh, done. Thanks for the feedback etc. I also tried benchmarking the changes but had trouble with antivirus stopping the runtime. |
Is it possible you can temporary disable the antivirus? Or add exclusion to the runtime and test folders to have the antivirus ignore them? |
Managed to get it working. Benchmarked some DateTime and TimeSpan parsing. Results are looking good IMO. Link to benchmarks: here
|
src/libraries/System.Private.CoreLib/src/System/Globalization/TimeSpanParse.cs
Outdated
Show resolved
Hide resolved
…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.Pow10
inDateTimeFormat
.long
toint
.