Optimise multiple Append and Prepend calls.#6864
Conversation
|
|
||
| public bool Append(T item, int number) | ||
| { | ||
| if (number != _count | number >= MaxLength) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Ditto here with my other line note; shouldn't it be > MaxLength?
83d3208 to
a49cdf9
Compare
|
Test this please |
|
Now why did dotnet-bot do that? (Was closing and re-opening seen as Spammy perhaps) |
|
Test Innerloop Windows_NT Release Build and Test (System.Net.Http.Functional.Tests.HttpClientHandlerTest.GetAsync_UnsupportedSSLVersion_Throws |
| public abstract int GetCount(bool onlyIfCheap); | ||
| } | ||
|
|
||
| private sealed class Append1Iterator<TSource> : AppendPrependIterator<TSource> |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
So have a single AppendPrepend with two collections?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
a49cdf9 to
cf498a9
Compare
|
Test Innerloop Windows_NT Debug Build and Test |
| 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); |
There was a problem hiding this comment.
@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: |
There was a problem hiding this comment.
minor question: is it worth having these integers as private named constants for readability?
There was a problem hiding this comment.
It's a linear progression through numbered states. I think the most readable labels are simply 1, 2, 3 etc.
| { | ||
|
|
||
| T[] newStore = new T[number << 1]; | ||
| for (int i = 0; i != store.Length; ++i) |
There was a problem hiding this comment.
Does using != as opposed to < here result in a performance improvement?
There was a problem hiding this comment.
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]; |
There was a problem hiding this comment.
Ditto here; leave a comment explaining it, or just put * 2 if the JIT already does this as an optimization.
cf498a9 to
c7aff48
Compare
| _count = 1; | ||
| } | ||
|
|
||
| public SharedCollection(T first, T second) |
There was a problem hiding this comment.
Nit: Maybe it would be more readable if you had a params T[] items constructor here instead?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
|
@dotnet-bot test Innerloop Windows_NT Debug Build and Test please |
|
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. |
Excellent point. I agree. |
|
Yes. That's the reason for the limit on the size of the shared collection, but perhaps a linked-list would be better. |
It's not necessarily the number that matters. One of the appended items could be a giant instance. |
|
That is true. A linked-list it shall be then. |
|
@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. |
|
@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. |
c7aff48 to
e128f2d
Compare
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; |
There was a problem hiding this comment.
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.
|
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.
e128f2d to
9857e37
Compare
|
That |
…_append_prepend_calls Optimise multiple Append and Prepend calls. Commit migrated from dotnet/corefx@2b60a4b
Use a more compact form for multiple calls to
AppendandPrepend, that shares a collection of appended and/or prepended elements between instances.Considering @stephentoub's comment at #5947 (comment)
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.