Implement IList<T> for EmptyPartition, RangeIterator and RepeatIterator.#37388
Implement IList<T> for EmptyPartition, RangeIterator and RepeatIterator.#37388aalmada wants to merge 1 commit intodotnet:masterfrom aalmada:RangeRepeatAsList
Conversation
Why would implementing We make no guarantees about the concrete type that's returned from these APIs, nor what interfaces they may or may not implement. Anyone who's casting should be doing so with a type check and a fallback. But if we were going to make a change to
I just spent half an hour looking through a bunch of code for use of Enumerable.Range(...) and Enumerable.Repeat(...). I found very few cases where they were directly followed by ToList and where performance mattered. At least in the code I looked at, by far the most common thing to follow Range was a Select, so I opened #37410. After that was ToArray, which is already handled by the RangeIterator implementation. Enumerable.Repeat itself is very rare in anything but test code I looked at.
They're not used in all platforms to avoid a bunch of additional generic instantiations and interface implementations when running on AOT platforms that care a lot about binary size. Adding I do appreciate your interest in contributing here. LINQ is an enticing area to add optimizations, with practically an infinite number of them possible. But it's infeasible for the implementation to provide optimizations for every possible combination of operator and input source, in terms of implementation and subsequent code maintainence, in terms of binary size, and in terms of the tradeoffs that result, and so we have basic implementations that work with everything and then only special-case those patterns we've found to be common and that are expected to make a meaningful difference on consuming code, and in particular in consuming code where such improvements actually matter. I'm not convinced this meets that bar. If you have real examples where it would, please do share; I'm happy to be convinced otherwise. |
@stephentoub I'm well aware that LINQ is a beast. I've been trying to contribute for while but it's been hard to find a spot where it doesn't break everything. Where, it can radically improves big data without affecting tiny collections. I'm also well aware that Range().ToArray() or Range().Select().ToArray() are way more common than Range().ToList(). This was a first attempt of a simple contribution, for the sake of code completeness and to evaluate acceptance criteria. I would think that #32645 is an example of someone getting confused. Also, I hope my comment there clarifies my opinion. I've been working on my own "green field" version of LINQ where I test my own ideas. I'm not worried about binary size but with performance on big collections. Looks like I'm going to stick to my work only but you're welcome to comment there. |
The contributions really are appreciated. As you suggest, though, it can be difficult to find places to make meaningful improvements. For several years we had a few contributors actively optimizing various parts of LINQ, and so lots of the low-hanging fruit has already been picked. Optimizations at this point generally involve tradeoffs that need to be scrutizined or alternatively necessitate data to demonstrate why the cost (e.g. extra code) is worth it in terms of the payoff.
The question there was, "is it reasonable for
Ok. Best of luck with your project. If you find things that you think would be valuable contributions back to System.Linq, we'd welcome the discussion. Thanks! |
|
When Just as an example, changing Another example is If |
EmptyEnumerable.Empty<T>has two implementations. TheSizeOptversion returnsArray.Empty<T>, while theSpeedOptversion returnsEmptyPartition<T>.Instance.Besides the different optimizations, there's another difference:
Array.Empty<T>returns an implementation ofIList<T>, whileEmptyPartition<T>.Instancereturns an implementation ofIEnumerable<T>.This difference may cause some confusion as it behaves differently in different platforms, not only between different versions, as already reported in #32645.
@natemcmaster This PR adds the implementation of
IList<T>as discussed on #32645. It also addsIReadOnlyList<T>.RangeandRepeatThese LINQ operations return implementations of
Iterator<T>which implementsIEnumerable<T>. But, for both these operations,Countis well known and the items can be inferred from theindexvalue. This means that these iterators can implementIList<T>.The advantage is that
ICollection<T>andIList<T>allow much better performance thanIEnumerable<T>in many situations. Many optimizations based on these interfaces can be found in this same repository and in many third-parties.Some of these cases may be overloaded by even better optimizations (
SpeedOptandSizeOpt) but these are not used in all platforms. Still, these changes, take advantage of existing code. No new cases are created.This PR adds implementation of
IList<T>andIReadOnlyList<T>to the iteratorsRangeIteratorandRepeatIterator<T>.Both
RangeandRepeatreturnEmpty<T>()whencountis zero. That's one other reason why it's importantEmptyIterator<T>also implementsIList<T>, reducing confusion.ToList()One of the cases that is overloaded in
SpeedOpt, is theToList()method for bothRangeIteratorandRepeatIterator<T>. They simply create aList<T>and add the elements to it. Here's the implementation for Range:Although the length is passed as the
capacityfor the newList<T>, theAdd()method still performs version increment, buffer length validation and size increment. This is performed for every element, which is unnecessary and penalizing.As an alternative, the
List<T>(IEnumerable<T>)constructor can be used, which has the following implementation:When
collectionis aICollection<T>, which is now the case, it simply allocates an array, callsICollection<T>.CopyTo()and sets the size.This PR replaces the
forloop withList.Add()calls by a single call to theList<T>(IEnumerable<T>)constructor.Benchmarks
These are the benchmarks for
Rangewith multiple count values (0, 1, 10, 100, 1.000, 10.000). The first line values are formasterbranch and the second for this PR branch.Please note that when
countis zero,RangereturnEmpty<int>(). The new version has the same performance.For the other cases where
RangeIteratoris returned, there is an initialization overhead due to the cast and argument validation. It's noticeable for very small values ofcount. It's 45% times slower whencountis 1.This overhead dilutes quickly when
countincreases. It takes more or less the same time whencountis 10. It's 30% faster whencountis 100, 40% faster whencountis 1.000 and 54% faster whencountis 10.000.In absolute values, this means that, it's only 16ns slower when
countis 1 but almost 10.000ns faster whencountis 10.000.There's no memory overhead added.