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

Optimise multiple Append and Prepend calls.#6864

Merged
VSadov merged 1 commit intodotnet:masterfrom
JonHanna:optimise_multiple_append_prepend_calls
May 26, 2016
Merged

Optimise multiple Append and Prepend calls.#6864
VSadov merged 1 commit intodotnet:masterfrom
JonHanna:optimise_multiple_append_prepend_calls

Conversation

@JonHanna
Copy link
Copy Markdown
Contributor

Use a more compact form for multiple calls to Append and Prepend, that shares a collection of appended and/or prepended elements between instances.

Considering @stephentoub's comment at #5947 (comment)

I'm imaging at some point we might want to create some optimized paths for when Append, Prepend, and Concat are used repetitively.

This attempts to optimise multiple append and prepends, but not concatenation.

It would need some more tests at the very least before its ready for merging, but I'd appreciate any input on the approach taken prior to that.


public bool Append(T item, int number)
{
if (number != _count | number >= MaxLength)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Is using | instead of || here intentional micro-optimization? If that's the case, it seems to me it's a very unusual pattern in corefx, so maybe it should be explicitly pointed out by a comment?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Its an as-a-rule rather than a measured micro-opt. As in "as a rule" just comparing integers will be cheaper than the cost of branching to avoid doing so, but you have to measure to know for sure.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Ditto here with my other line note; shouldn't it be > MaxLength?

@JonHanna JonHanna force-pushed the optimise_multiple_append_prepend_calls branch from 83d3208 to a49cdf9 Compare March 30, 2016 16:02
@JonHanna JonHanna changed the title Optimise multiple Append and Prepend calls. [WIP] Optimise multiple Append and Prepend calls. Mar 30, 2016
@JonHanna
Copy link
Copy Markdown
Contributor Author

Test this please

@JonHanna
Copy link
Copy Markdown
Contributor Author

Now why did dotnet-bot do that? (Was closing and re-opening seen as Spammy perhaps)

@JonHanna
Copy link
Copy Markdown
Contributor Author

Test Innerloop Windows_NT Release Build and Test

(System.Net.Http.Functional.Tests.HttpClientHandlerTest.GetAsync_UnsupportedSSLVersion_Throws
Timeout caused cancelling? http://dotnet-ci.cloudapp.net/job/dotnet_corefx/job/windows_nt_release_prtest/4306/testReport/junit/System.Net.Http.Functional.Tests/HttpClientHandlerTest/GetAsync_UnsupportedSSLVersion_Throws_name___SSLv3___url___https___www_ssllabs_com_10300___/)

public abstract int GetCount(bool onlyIfCheap);
}

private sealed class Append1Iterator<TSource> : AppendPrependIterator<TSource>
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Rather than duplicating the type hierarchy for append vs prepend, my preference would be to carry a bool around and switch on it in the implementation. It's not much more extra space required, the branch should be negligible compared to all of the interface calls being made, and the amount of code required and associated complexity should shrink non-trivially.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

So have a single AppendPrepend with two collections?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Ah, right, I hadn't gotten that far. But, yeah, rather than having a single collection field, have two collection fields, one of which will likely be null. We increase the size of the object by a single reference field, but cut back on complexity and duplication. I think I'm suggesting just get rid of your AppendN and PrependN iterators, in favor of just using AppendPrependNIterator.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

And also for this Append1 case, I'm suggesting having a single AppendPrepend1Iterator with an extra bool field rather than having two different Append1 and Prepend1 cases, and use that bool to determine whether the single _element is meant to be appended or prepended.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

That would cut down the size, but unless it's all cut down to one class we're either left with still having the interface call, or adding to the number of type checks in the query method.

Of course the case with two collections can handle the Append1 and Prepend1 case too, and maybe the extra weight of that is worth the extra simplicity.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'm not following. Why would it change the number of type checks or interface calls? It would add branches, yes, because you'd need to check whether the bool was false or true to determine whether to append or prepend the element... why is it more than that? I'm suggesting there just be three types, as there is in the concat case: AppendPrependIterator, AppendPrepend1Iterator, and AppendPrependNIterator. The first is the abstract base, the second contains a single element and a bool to say whether it's an append or prepend, and the third contains two fields for two collections, one for each of append/prepend, and they may be null if there's none of that case.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Maybe it's me that's not following. I read you as suggesting an AppendPrependNIterator and an AppendPrepend1Iterator as separate classes, in which case to Append to one of those we need to either keep the abstract model in use here (so we don't lose the virtual call) or check for both of those types rather than just checking for one.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I read you as suggesting an AppendPrependNIterator and an AppendPrepend1Iterator as separate classes

Yes.

in which case to Append to one of those we need to either keep the abstract model in use here (so we don't lose the virtual call)

Yes. I wasn't suggesting that my approach would be cheaper, but it also shouldn't add more virtual calls than what you already have, and it effectively cuts the amount of code in half.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Ah. I see what I misread now.

…negligible compared to all of the interface calls being made

I misread that as suggesting that said calls would be compensatorily reduced.

@JonHanna JonHanna force-pushed the optimise_multiple_append_prepend_calls branch from a49cdf9 to cf498a9 Compare April 9, 2016 11:20
@JonHanna
Copy link
Copy Markdown
Contributor Author

JonHanna commented Apr 9, 2016

Test Innerloop Windows_NT Debug Build and Test
Test Innerloop Windows_NT Release Build and Test
(Hung process, and SSL tests at http://dotnet-ci.cloudapp.net/job/dotnet_corefx/job/windows_nt_release_prtest/4672/ which might be a network fluke, will open an issue if it repeats).

public override List<TSource> ToList()
{
int count = GetCount(onlyIfCheap: true);
List<TSource> list = count == -1 ? new List<TSource>(Math.Max(_appCount + _preCount, 4)) : new List<TSource>(count);
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@stephentoub I dropped the new List<TSource>(4) from the other case, but I've left Math.Max(_appCount + _preCount, 4) as it seems to me that "the sum of the sizes of the two parts we know are adding in for sure, unless that's pretty low in which case a wee bit more" is a reasonable heuristic for the starting size, with 4 as a reasonable value for "wee bit more".

{
switch (_state)
{
case 1:
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

minor question: is it worth having these integers as private named constants for readability?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

It's a linear progression through numbered states. I think the most readable labels are simply 1, 2, 3 etc.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

sure

{

T[] newStore = new T[number << 1];
for (int i = 0; i != store.Length; ++i)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Does using != as opposed to < here result in a performance improvement?

Copy link
Copy Markdown
Contributor Author

@JonHanna JonHanna Apr 17, 2016

Choose a reason for hiding this comment

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

I very much doubt it. Preference from a long time ago when more often using C++ iterator objects where != might be defined for the type but < might not. (And longer ago again it might perhaps have made for a slight but measurable performance difference on old CPUs).

if (store.Length == number)
{

T[] newStore = new T[number << 1];
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Ditto here; leave a comment explaining it, or just put * 2 if the JIT already does this as an optimization.

@JonHanna JonHanna force-pushed the optimise_multiple_append_prepend_calls branch from cf498a9 to c7aff48 Compare April 17, 2016 13:46
_count = 1;
}

public SharedCollection(T first, T second)
Copy link
Copy Markdown
Contributor

@jamesqo jamesqo Apr 17, 2016

Choose a reason for hiding this comment

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

Nit: Maybe it would be more readable if you had a params T[] items constructor here instead?

Copy link
Copy Markdown
Member

@stephentoub stephentoub Apr 17, 2016

Choose a reason for hiding this comment

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

Maybe it would be more readable if you had a params T[] items constructor here instead?

That would allocate an array at the call site, and while there's an array being allocated here internally, it wouldn't be clear to this code whether the array being passed in was having ownership transferred, so it would likely need to make a copy to protect itself.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

We could assign it straight to _store so it wouldn't add extra cost, but this makes it clear that it's only ever called with 1 or 2 elements.

@VSadov
Copy link
Copy Markdown
Member

VSadov commented Apr 29, 2016

@dotnet-bot test Innerloop Windows_NT Debug Build and Test please
@dotnet-bot test Innerloop CentOS7.1 Release Build and Test and Test please

@VSadov
Copy link
Copy Markdown
Member

VSadov commented Apr 29, 2016

I have concerns about attaching appended elements to iterator through which they are not reachable. It seems it can lead to unexpected retention.

// here a,b,c become GC-reachable from x, but not iterator-reachable
x.Append(a).Append(b).Append(c);  

I think prepend case could be handled by sharing a single-linked list. Then whatever is reachable via iteration is also what is GC-reachable.

Not sure if similar solution is possible for Append case.

@stephentoub
Copy link
Copy Markdown
Member

I have concerns about attaching appended elements to iterator through which they are not reachable

Excellent point. I agree.

@JonHanna
Copy link
Copy Markdown
Contributor Author

Yes. That's the reason for the limit on the size of the shared collection, but perhaps a linked-list would be better.

@stephentoub
Copy link
Copy Markdown
Member

That's the reason for the limit on the size of the shared collection

It's not necessarily the number that matters. One of the appended items could be a giant instance.

@JonHanna
Copy link
Copy Markdown
Contributor Author

That is true. A linked-list it shall be then.

@JonHanna
Copy link
Copy Markdown
Contributor Author

@svick there's a similar thing done with Concat. Maybe it will turn out to not be worth it upon trying, but I shall take a look.

@svick
Copy link
Copy Markdown
Member

svick commented Apr 29, 2016

@JonHanna Sorry, I deleted my comment. I forgot that the current implementation means iterating is quadratic, so a linked list would probably still be worth it.

@JonHanna JonHanna force-pushed the optimise_multiple_append_prepend_calls branch from c7aff48 to e128f2d Compare May 2, 2016 03:09
@JonHanna
Copy link
Copy Markdown
Contributor Author

JonHanna commented May 2, 2016

Not sure if similar solution is possible for Append case.

I wasn't sure either when I first approached this, and so had rejected a linked-list approach, but if we keep track of the size of the list (which is worth doing anyway, for other optimisations) we can store the appended items in an array as they are being iterated, and so gather them up in a single O(n) sweep before an O(n) iteration through them, rather than having the quadratic behaviour.

_threadId = Environment.CurrentManagedThreadId;
}

protected bool RunningOnCreatingThread => _threadId == Environment.CurrentManagedThreadId;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Is this used?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I don't think it is after the last round of changes, so it can be reverted. I'm not having a lot of time for CoreFX right now, but I'll try to tidy this up ASAP.

@VSadov
Copy link
Copy Markdown
Member

VSadov commented May 15, 2016

Looks like a good compromise. Append case needs an allocation of an array, but I do not see a lot of alternatives. Without that we have the n^2 problem when iterating.

RunningOnCreatingThread seems to be a method not used anywhere.

Otherwise LGTM.

Use a more compact form for multiple calls to Append and Prepend, that
shares a collection of appended and/or prepended elements between
instances.
@JonHanna JonHanna force-pushed the optimise_multiple_append_prepend_calls branch from e128f2d to 9857e37 Compare May 26, 2016 14:05
@JonHanna
Copy link
Copy Markdown
Contributor Author

That RunningOnCreatingThread cruft has been removed.

@VSadov VSadov merged commit 2b60a4b into dotnet:master May 26, 2016
@JonHanna JonHanna deleted the optimise_multiple_append_prepend_calls branch May 27, 2016 00:30
@karelz karelz modified the milestone: 1.1.0 Dec 3, 2016
picenka21 pushed a commit to picenka21/runtime that referenced this pull request Feb 18, 2022
…_append_prepend_calls

Optimise multiple Append and Prepend calls.

Commit migrated from dotnet/corefx@2b60a4b
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.