Thanks to visit codestin.com
Credit goes to github.com

Skip to content
This repository was archived by the owner on Jan 23, 2023. It is now read-only.

Conversation

Petermarcu
Copy link
Member

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


if (format.Length == 1) {
switch (format[0]) {
case 'o':
Copy link
Member

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.

Copy link
Member Author

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);
}
Copy link
Member

@tarekgh tarekgh Oct 26, 2016

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

Copy link
Member Author

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.

Copy link
Member Author

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.

Copy link
Member Author

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.

Copy link
Member

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;
            }
        }

Copy link

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)
Copy link
Member

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

@tarekgh
Copy link
Member

tarekgh commented Oct 26, 2016

@Petermarcu other than my minor comments, LGTM.

I'm seeing an ~90% reduction in allocation count and ~80% reduction in allocated bytes after this change.

just curious how did you measure it?

}


internal static string RoundTripFormat(ref DateTime dateTime)
Copy link
Member

@tarekgh tarekgh Oct 27, 2016

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";

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good feedback

Copy link
Member Author

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.

@Petermarcu
Copy link
Member Author

@dotnet-bot test Linux ARM Emulator Cross Debug Build please

@Petermarcu
Copy link
Member Author

@dotnet-bot test Linux ARM Emulator Cross Release Build please

case 'o':
case 'O':
realFormat = RoundtripFormat;
break;
Copy link
Member

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?

Copy link
Member

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

}


internal static string RoundTripFormat(ref DateTime dateTime)
Copy link
Member

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?

Copy link
Member

@tarekgh tarekgh Oct 27, 2016

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;
Copy link
Member

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

val = val / 10;
index++;
}
}
Copy link

@redknightlois redknightlois Oct 28, 2016

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 ;)

@Petermarcu
Copy link
Member Author

@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++;
}
Copy link
Member

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

@tarekgh
Copy link
Member

tarekgh commented Oct 28, 2016

I put another minor comment, other than that LGTM

@Petermarcu
Copy link
Member Author

Thanks. I've addressed all feedback. I'll commit once green.

@Petermarcu
Copy link
Member Author

@dotnet-bot test Windows_NT x64 Release please

@Petermarcu
Copy link
Member Author

@dotnet-bot test Windows_NT x64 Release Priority 1 Build and Test please

@Petermarcu Petermarcu merged commit 026beb9 into dotnet:master Oct 29, 2016
@Petermarcu Petermarcu deleted the DateTime branch October 29, 2016 04:26
@weshaggard
Copy link
Member

@jkotas
Copy link
Member

jkotas commented Oct 30, 2016

Opened dotnet/corert#2109 so that we do not forget to port it.

Copy link

@ayende ayende left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

internal static string RoundTripFormat(ref DateTime dateTime)
{
StringBuilder result = StringBuilderCache.Acquire();
Append(result, dateTime.Year, 4);
Copy link

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);
}
Copy link

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.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

10 participants