-
Notifications
You must be signed in to change notification settings - Fork 2.6k
DateTime.ToString(“o”) allocates 32 objects, #7836
Conversation
@@ -957,12 +949,88 @@ internal static String Format(DateTime dateTime, String format, DateTimeFormatIn | |||
} | |||
|
|||
if (format.Length == 1) { | |||
switch (format[0]) { | |||
case 'o': |
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.
nit: using if-statement maybe better than switch here as we are testing 2 values.
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 will do that, it was another thing I debated and switches seemed to be used so much in this file I went with the style of the file :)
if (digit > 1) | ||
{ | ||
Append(builder, val / 10, digit - 1); | ||
} |
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.
this may be expensive as it will be recursive call. I would do it in a loop better
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.
Yeah, I was torn because its limited in its current use to not being too deep but I can convert to a loop.
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.
Not really happy with my options for a loop here. Will still think about it but may stick with recursion if I can't come up with a better idea for how to do it in a loop cheaply.
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.
Every solution I've come up with for the loop requires more allocations and the use of a stack of some kind to append the digits from most significant to least. Given the max recursive depth here is 7 and its usually 2 I think the recursive solution is fine and the code is much more readable.
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.
here is some implementation suggestion but I'll leave it to you to decide if you think the recursion is better here.
internal static void AppendNumber(StringBuilder builder, long val, int digit)
{
for (int i = 0; i < digit; i++)
{
builder.Append('0');
}
int index = 1;
while (val > 0 && index <= digit)
{
builder[builder.Length - index] = (char)('0' + (val % 10));
val = val / 10;
index++;
}
index = builder.Length - digit;
while (val > 0)
{
builder.Insert(index, (char)('0' + (val % 10)));
val = val / 10;
}
}
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 usage of the string builder is expensive. You are first allocating the string builder (even cached), then in the end you are allocating the string each and every time.
We have run into perf issues here and we solved them in the following manner:
https://ayende.com/blog/169798/excerpts-from-the-ravendb-performance-team-report-dates-take-a-lot-of-time
Note that this code does a single allocation for the string, compute the date parts only once.
In our tests, it was 15% of the runtime of the default impl. And it can probably be improved still.
return StringBuilderCache.GetStringAndRelease(result); | ||
} | ||
|
||
internal static void Append(StringBuilder builder, long val, int digit) |
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.
Append [](start = 29, length = 6)
nit: could you call this AppendNumber instead. it will make easier to not confuse it with the StringBuilder Append
@Petermarcu other than my minor comments, LGTM.
just curious how did you measure it? |
|
||
return (FormatCustomized(dateTime, format, dtfi, offset)); | ||
} | ||
|
||
|
||
internal static string RoundTripFormat(ref DateTime dateTime) |
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.
RoundTripFormat [](start = 31, length = 15)
one last thing, you named this method as RoundtripFormat which is same name as the defined string constant. would be better to rename the method to something better (e.g. FormatAsISO8601() or anything you think better).
internal const String RoundtripFormat = "yyyy'-'MM'-'dd'T'HH':'mm':'ss.fffffffK";
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.
good feedback
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 measured it using the Perf tools in Visual Studio. I can show you if you'd like to see it.
@dotnet-bot test Linux ARM Emulator Cross Debug Build please |
@dotnet-bot test Linux ARM Emulator Cross Release Build please |
case 'o': | ||
case 'O': | ||
realFormat = RoundtripFormat; | ||
break; |
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.
Removing this doesn't break DateTime.Parse?
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.
Actually this is a good catch. the parsing code use this code path too. through ExpandPredefinedFormat which called from DoStrictParse
|
||
return (FormatCustomized(dateTime, format, dtfi, offset)); | ||
} | ||
|
||
|
||
internal static string RoundTripFormat(ref DateTime dateTime) |
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.
Why ref DateTime dateTime
rather than just DateTime dateTime
?
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.
right, DateTime is long size so it can passed as value
break; | ||
|
||
default: | ||
break; |
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.
Nit: this default case could be removed
builder[builder.Length - index] = (char)('0' + (val % 10)); | ||
val = val / 10; | ||
index++; | ||
} | ||
} |
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.
Given that AppendNumber
is used multiple times in the same method, I would look if there is a performance improvement by aggresively inlining AppendNumber
. JIT may recognize it and build a single function pushing jump offsets to the stack. That would ensure that the code can avoid the call overhead altogether.
EDIT: If not, it's probably worth to open an issue for the JIT team ;)
@tarekgh , Can you take one last look? I updated it to handle the DateTimeOffset failure I was seeing and to leverage an existing method I found while investigating. All other feedback was incorporated. @redknightlois, I didn't see a noticable difference with and without the attribute. If you want to try and measure the overhead and see if there is something actionable for the JIT team, that would be awesome! For now, I'm going to trust the JIT on this one :) |
builder[builder.Length - index] = (char)('0' + (val % 10)); | ||
val = val / 10; | ||
index++; | ||
} |
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 seeing you have removed the second loop. I would suggest we just assert val == 0 if we need this method be reliable in case someone else later use it wrong way
I put another minor comment, other than that LGTM |
Thanks. I've addressed all feedback. I'll commit once green. |
@dotnet-bot test Windows_NT x64 Release please |
@dotnet-bot test Windows_NT x64 Release Priority 1 Build and Test please |
FYI - We have this same code in corert https://github.com/dotnet/corert/blob/master/src/System.Private.CoreLib/src/System/Globalization/FormatProvider.DateTimeFormat.cs. |
Opened dotnet/corert#2109 so that we do not forget to port it. |
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 might be a better way to handle this, see:
https://ayende.com/blog/169798/excerpts-from-the-ravendb-performance-team-report-dates-take-a-lot-of-time
internal static string RoundTripFormat(ref DateTime dateTime) | ||
{ | ||
StringBuilder result = StringBuilderCache.Acquire(); | ||
Append(result, dateTime.Year, 4); |
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.
Note that calling .Year
, .Month
, etc are all doing quite a bit of work, and you are doing this each and every time.
This is computationally expensive, and you can probably avoid doing this if you build the entire thing in one go.
if (digit > 1) | ||
{ | ||
Append(builder, val / 10, digit - 1); | ||
} |
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 usage of the string builder is expensive. You are first allocating the string builder (even cached), then in the end you are allocating the string each and every time.
We have run into perf issues here and we solved them in the following manner:
https://ayende.com/blog/169798/excerpts-from-the-ravendb-performance-team-report-dates-take-a-lot-of-time
Note that this code does a single allocation for the string, compute the date parts only once.
In our tests, it was 15% of the runtime of the default impl. And it can probably be improved still.
Fix for #7815
I'm still working on running the corefx tests on this change and possibly adding some but wanted to start the CR process.
I'm seeing an ~90% reduction in allocation count and ~80% reduction in allocated bytes after this change.
@tarekgh @weshaggard @stephentoub