Skip to content

Optimize GreenNode.CreateList#48568

Merged
333fred merged 15 commits intodotnet:masterfrom
HurricanKai:optimize-greennode-createlist
Oct 25, 2020
Merged

Optimize GreenNode.CreateList#48568
333fred merged 15 commits intodotnet:masterfrom
HurricanKai:optimize-greennode-createlist

Conversation

@HurricanKai
Copy link
Member

@HurricanKai HurricanKai commented Oct 13, 2020

This PR improves the performance of GreenNode.CreateList.
This is part of a larger goal to improve NormalizeWhitespace performance, the benchmark below tests NormalizeWhitespace performance of real-world generated C# source (~116k loc when normalized).
Benchmark:

Method Mean Error StdDev Gen 0 Gen 1 Gen 2 Allocated
Baseline 5.806 s 0.0556 s 0.0493 s 74000.0000 18000.0000 - 591.82 MB
New 5.595 s 0.1072 s 0.1731 s 49000.0000 13000.0000 - 393.68 MB

Tested on Net5.0 RC2 with the Benchmark runtime being .NET Core 3.1.8

I also investigated a couple of other approaches (ImmutableArray + SelectAsArray, GetEnumerator directly) but this one came out on top.
Thanks for the help @333fred and @CyrusNajmabadi 😄

Benchmark Files (drop into IdeCoreBenchmarks for testing):
normalization-sample.txt
NormalizeWhitespaceBenchmark.cs

@HurricanKai HurricanKai requested a review from a team as a code owner October 13, 2020 20:14
@Dotnet-GitSync-Bot
Copy link
Collaborator

I couldn't figure out the best area label to add to this PR. If you have write-permissions please help me learn by adding exactly one area label.

@333fred 333fred added Area-Compilers Community The pull request was submitted by a contributor who is not a Microsoft employee. labels Oct 13, 2020
Copy link
Member

@333fred 333fred left a comment

Choose a reason for hiding this comment

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

Overall looks good, a couple of issues.

@HurricanKai
Copy link
Member Author

HurricanKai commented Oct 14, 2020

Here are some performance figures. The currently committed version is List, previously it was IROList and the current master is Baseline, the others are for other alternatives I've considered.
The numbers aren't 100% accurate but should give a good idea what's fastest. Sorted by speed.

Method Mean Error StdDev Gen 0 Gen 1 Gen 2 Allocated
List 5.683 s 0.0338 s 0.0316 s 72000.0000 18000.0000 - 579.89 MB
List/Array 5.861 s 0.0505 s 0.0472 s 67000.0000 17000.0000 - 536.25 MB
IROList 5.889 s 0.0371 s 0.0310 s 49000.0000 13000.0000 - 393.68 MB
Baseline 5.912 s 0.0356 s 0.0316 s 74000.0000 18000.0000 - 591.82 MB

@CyrusNajmabadi
Copy link
Contributor

Based on those numbers, looks like we should stick with IROL and not use List. Or, we have an overload for both or something (but htat seems like a lot of complexity).

@333fred
Copy link
Member

333fred commented Oct 15, 2020

Is the List<T> version generating more garbage from the non-List paths? Looking at those numbers, while using IROL list a clear win in terms of memory, it doesn't actually make a different CPU-time-wise, as the IROL and Baseline are within the Error margin. I think I'd prefer to have both a List and IROL overload. The method complexity is not very high, and 200 MB of memory savings on a large NormalizeWhitespace, while nice, is basically peanuts for our scenarios.

@HurricanKai
Copy link
Member Author

The only types every passed in are List<T> and T[] I've tested two overloads with those two, see List/Array, I'll test List/IROList too, but I'd be surprised if it yielded better/much different results. Maybe because most type passed to WithTrivia are IROList 🤔

@HurricanKai
Copy link
Member Author

I've just tested this but the results aren't beyond some error, so I doubt it makes a difference.

@333fred
Copy link
Member

333fred commented Oct 20, 2020

I've just tested this

What did you test?

@HurricanKai
Copy link
Member Author

Having both an overload for List & IROList
This is only useful in the WithTrivia case, so there the code I chose was

null => null,
List<TFrom> list => GreenNode.CreateList(list, select),
IReadOnlyList list => GreenNode.CreateList(list, select),
_ => GreenNode.CreateList(enumerable.ToList(), select)

Copy link
Member

@333fred 333fred left a comment

Choose a reason for hiding this comment

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

Just one more comment

Copy link
Member

@333fred 333fred left a comment

Choose a reason for hiding this comment

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

LGTM (commit 9). @dotnet/roslyn-compiler for a second review of this community perf improvement.

@HurricanKai
Copy link
Member Author

HurricanKai commented Oct 21, 2020

I've updated the original post to show current numbers.
Not sure what those formatting errors are about 🤔

default: {
var array = new ArrayElement<GreenNode>[list.Count];
for (int i = 0; i < array.Length; i++)
array[i].Value = select(list[i]);
Copy link
Contributor

Choose a reason for hiding this comment

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

📝 Even though list was already declared as having non-null GreenNode items, the new behavior here is slightly different for the case where a null value appeared. I'm not sure if that's a problem or not.

var list = nodes.ToArray();

switch (list.Length)
public static GreenNode? CreateList<TFrom>(IReadOnlyList<TFrom> list, Func<TFrom, GreenNode> select)
Copy link
Contributor

Choose a reason for hiding this comment

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

💭 This does make me wonder if we could somehow change this to only have one overload that takes ReadOnlySpan<TFrom>. Perhaps for the future...

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 also thought so. Note however that the only case where this is used is from WithLeading/Trailing trivia, and in those cases it seems more beneficial to check for IROList. See the List/Array benchmark above, it's not Span, but it indicates less special = better. And if I'm not missing anything ROSpan is a rare thing to be provided. (we could look into special casing for Array/Memory/ROMemory/Span/ROSpan and call into the ROSpan overload in all those cases. Will try and maybe open another PR)

Copy link
Contributor

Choose a reason for hiding this comment

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

And if I'm not missing anything ROSpan is a rare thing to be provided

ReadOnlySpan<T> cannot be cast to IEnumerable<T>, so to use this form all the callers would need to be updated to provide the correct type.

@HurricanKai
Copy link
Member Author

Not sure why those tests failed. Seems like something timed out?

@333fred 333fred merged commit 8888978 into dotnet:master Oct 25, 2020
@ghost ghost added this to the Next milestone Oct 25, 2020
@333fred
Copy link
Member

333fred commented Oct 25, 2020

Thanks @HurricanKai!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Area-Compilers Community The pull request was submitted by a contributor who is not a Microsoft employee.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants