-
Notifications
You must be signed in to change notification settings - Fork 5.2k
Implements new intand long overloads for TimeSpan
#94143
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
Conversation
|
Note regarding the This serves as a reminder for when your PR is modifying a ref *.cs file and adding/modifying public APIs, please make sure the API implementation in the src *.cs file is documented with triple slash comments, so the PR reviewers can sign off that change. |
|
@dotnet-policy-service agree |
|
Resolves #93890 |
|
Tagging subscribers to this area: @dotnet/area-system-datetime Issue DetailsI went ahead and took the liberty to add an
|
It will need API review if it wasn't approved... that may be quick. |
| return new TimeSpan((long)days * TicksPerDay); | ||
| } | ||
|
|
||
| public static TimeSpan FromDays(int days, int hours = 0, long minutes = 0, long seconds = 0, long milliseconds = 0, long microseconds = 0) |
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.
public static TimeSpan FromDays(int days, int hours = 0, long minutes = 0, long seconds = 0, long milliseconds = 0, long microseconds = 0)
Please look at the code of the constructor public TimeSpan(int days, int hours, int minutes, int seconds, int milliseconds, int microseconds). It has some logic to ensure no overflow by checking the total max microseconds. I suggest you refactor this code into its own method and then call it from here and from the constructor.
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.
wait, we are using long and not int for the parameters. I think we need to implement this to ensure there is no overflow will occur. will not be able to share the code with the constructor :-(
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.
@tarekgh I'm struggling to see the benefit of manually checking for overflows given that long.MaxValue represents the upper limit for TimeSpan's length. Any computation exceeding this limit inherently triggers an OverflowException. Implementing explicit checks seems to introduce additional CPU overhead, thus potentially reducing performance.
Here are some benchmarks:
| Method | Input | Mean | Error | StdDev | Gen0 | Allocated |
|---------------------------------- |-------------------- |--------------:|-----------:|-----------:|-------:|----------:|
| DefaultOverflowException | 9223372036854775807 | 0.0192 ns | 0.0177 ns | 0.0165 ns | - | - |
| ManualOverflowChecksThenException | 9223372036854775807 | 3,188.9095 ns | 42.1336 ns | 39.4118 ns | 0.0229 | 240 B |
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.
Any computation exceeding this limit inherently triggers an OverflowException
No. this will trigger it if the operation is checked which we don't enable by default. https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/statements/checked-and-unchecked
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.
Any computation exceeding this limit inherently triggers an OverflowException
No. this will trigger it if the operation is
checkedwhich we don't enable by default. https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/statements/checked-and-unchecked
I see, and what are the implications of having the checked keyword being used explicitly in there?
I re-did the benchmarks and seems like using the checked context is in fact slower than manually checking for overflows.
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.
That is what I was trying to point at in my comment #94143 (comment)
| return new TimeSpan((long)hours * TicksPerHour); | ||
| } | ||
|
|
||
| public static TimeSpan FromHours(int hours, long minutes = 0, long seconds = 0, long milliseconds = 0, long microseconds = 0) |
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.
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.
Waiting for your reply in the review up there
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.
replied. We can try to optimize the checking as much as we can but have to ensure we produce the correct behavior.
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.
enable checked still have some perf cost too and we'll need to decide if we need to catch the thrown overflow exception and convert it to something like ThrowHelper.ThrowArgumentOutOfRange_TimeSpanTooLong(). @bartonjs do you know if we have any design guidelines around using checked in the APIs? I did some search but couldn't find any.
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.
@tannergooding do you see any trick to efficiently detect overflow for this particular calculation? Something better than checking whether the sign changed after each multiplication, or doing it all in an int128
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.
The CheckedKeyword results also look decent. Considering exceptions should be rare, the extra 10 microseconds and 8 bytes might not be a big deal.
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’d just go with the simple approach I detailed above as it lends the most capability for the JIT to optimize it.
We can revisit and optimize further if required later. Some of the easiest optimizations will be low hanging fruit such as improving operator +
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.
That is, I'd go with doing this:
public static TimeSpan FromHours(int hours, long minutes = 0, long seconds = 0, long milliseconds = 0, long microseconds = 0)
{
return FromHours(hours)
+ FromMinutes(minutes)
+ FromSeconds(seconds)
+ FromMilliseconds(milliseconds)
+ FromMicroseconds(microseconds);
}The other patterns will end up with less capability for the JIT to optimize, especially for constant inputs, and that is overall less desirable.
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 opened #94226 to fix up some of the cases like operator + so that the relevant inlining can occur.
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 opened #94226 to fix up some of the cases like
operator +so that the relevant inlining can occur.
Great, I already have the solution you wanted implemented, now I just have to write a few unit tests for it, although I'm not really certain what to test in specific. I guess I should test overflows and correct TimeSpans?
| return Interval(value, TicksPerDay); | ||
| } | ||
|
|
||
| public static TimeSpan FromDays(int days) |
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.
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.
Haha I originally wrote it with the XML Docs but noticed that the older methods do not have any documentation and removed them. No problem.
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.
Yes we are slowly adding missing XML docs through these libraries. Historically docs were not generated from sources (unlike most .NET libraries out there - perhaps because many were written so early that there was not a useful docs tool chain). As we copy the official docs back onto the API (aided by tools) we can switch one library after another to being the "source of truth" for its API docs. Eventually they all will be. There is an ongoing project for this in case you are interested in contributing to it at some point.
It doesn't make sense to have |
The main benefit appears to be the conservation of CPU cycles. Otherwise, why have both |
Agree, in the approved overloads, the second parameter shouldn't be defaulted on. @bartonjs @tannergooding do you have thoughts about that? |
Btw, if the purpose of these methods is to save CPU cycles, wouldn't this imply that we need to implement overflow checks repeatedly? Without these checks, we'd still incur the CPU cost for computations like |
I don't think this is the main goal. The main goal is to provide API produce accurate results no rounding or truncation as the overloads that take double. Still be good to optimize as much as we can as a secondary goal. |
I understand that delivering precise results is the primary objective. However, my point is that the rationale for having distinct methods like |
To be honest, I have a different perspective. The "From" methods we offer allow you to create a time span without needing to provide all the parameters that constructors require. This is why we are utilizing the default parameters. I am not saying we should not care about performance. I am trying to say we should first ensure the precision and user experience of the APIs usage first and then try to make the APIs performant without scarifying the main goal. |
But from the user's perspective, what would be the difference if we removed the |
Usability studies have shown that junior/novice developers get confused with the IntelliSense presentation of methods with many default parameters. That resulted in this guideline in Framework Design Guidelines, 3rd edition (specifically in 5.1.1 (Member Overloading), p143): |
|
There's also room for improved performance (from various reasons) from the shorter overloads, but the reason they got added in API Review was because of the usability guideline, not a performance goal. |
Should we add Also, in the approved APIs, should all overloads that taking optional parameters has the second argument be not defaulted? like public static TimeSpan FromDays(int days);
public static TimeSpan FromDays(int days, int hours = 0, long minutes = 0, long seconds = 0, long milliseconds = 0, long microseconds = 0);the hours parameter shouldn't be defaulted. or do we have this if someone uses named arguments? |
No, because the current method for it does not have two or more defaulted parameters. It has one mandatory parameter and one optional/defaulted parameter.
The way that it got approved is correct. The number of hours in FromDays should, indeed, be defaulted so that someone can call |
|
@tcortega let's continue with the original approved proposal. Could you please update the code according to the discussions here? Thanks! |
|
This pull request has been automatically marked |
|
@tcortega are you willing to finish this PR. @stefannikolei is interested in helping too. Thanks! |
Hey, I can't do it right now, so @stefannikolei, feel free to go ahead with it. Thanks! |
|
@stefannikolei please create a new PR based on the discussions in this PR. Thanks @tcortega for letting us know. |
I went ahead and took the liberty to add an
public static System.TimeSpan FromMilliseconds(long milliseconds);that was not detailed in the proposal, but felt like it does not make sense to not add it.