[Withdrawn] Pre-size the first Enumerable.Chunk buffer (no-go)#131028
[Withdrawn] Pre-size the first Enumerable.Chunk buffer (no-go)#131028artl93 wants to merge 1 commit into
Conversation
For countable non-array sources, size the first chunk buffer from a non-enumerating count hint (TryGetNonEnumeratedCount) instead of starting at 4 and growing 4->8->...->N via repeated Array.Resize. Lazy/uncountable sources keep the small-start behavior and are never force-enumerated. Fixes dotnet#115079 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: d7060115-2ee5-46f4-b6e0-acad31d5a484
|
@EgorBot -linux_amd -osx_arm64 using System.Collections;
using System.Linq;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
BenchmarkSwitcher.FromAssembly(typeof(Bench).Assembly).Run(args);
// A non-array ICollection<T>: countable via TryGetNonEnumeratedCount, but exercises
// the EnumerableChunkIterator path rather than the array fast path.
public sealed class CountableCollection<T> : ICollection<T>
{
private readonly T[] _items;
public CountableCollection(T[] items) => _items = items;
public int Count => _items.Length;
public bool IsReadOnly => true;
public IEnumerator<T> GetEnumerator() => ((IEnumerable<T>)_items).GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => _items.GetEnumerator();
public bool Contains(T item) => Array.IndexOf(_items, item) >= 0;
public void CopyTo(T[] a, int i) => _items.CopyTo(a, i);
public void Add(T item) => throw new NotSupportedException();
public void Clear() => throw new NotSupportedException();
public bool Remove(T item) => throw new NotSupportedException();
}
// Genuinely lazy source with unknown count: TryGetNonEnumeratedCount returns false,
// so the source must never be force-enumerated for a count.
public sealed class LazyRange : IEnumerable<int>
{
private readonly int _count;
public LazyRange(int count) => _count = count;
public IEnumerator<int> GetEnumerator()
{
for (int i = 0; i < _count; i++) yield return i;
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
[MemoryDiagnoser]
public class Bench
{
[Params(1024, 8192)]
public int Count { get; set; }
[Params(16, 256, 1024)]
public int Size { get; set; }
private int[] _array = default!;
private List<int> _list = default!;
private CountableCollection<int> _coll = default!;
private LazyRange _lazy = default!;
[GlobalSetup]
public void Setup()
{
_array = Enumerable.Range(0, Count).ToArray();
_list = _array.ToList();
_coll = new CountableCollection<int>(_array);
_lazy = new LazyRange(Count);
}
private static int Consume(IEnumerable<int[]> chunks)
{
int total = 0;
foreach (int[] c in chunks) total += c.Length;
return total;
}
[Benchmark] public int Array() => Consume(((IEnumerable<int>)_array).Chunk(Size));
[Benchmark] public int List() => Consume(_list.Chunk(Size));
[Benchmark] public int Collection() => Consume(_coll.Chunk(Size));
[Benchmark] public int Lazy() => Consume(_lazy.Chunk(Size));
}Note This benchmark request was created with GitHub Copilot. |
|
Azure Pipelines: Successfully started running 3 pipeline(s). 13 pipeline(s) were filtered out due to trigger conditions. There may be pipelines that require an authorized user to comment /azp run to run. |
|
Tagging subscribers to this area: @dotnet/area-system-linq |
There was a problem hiding this comment.
Pull request overview
Updates Enumerable.Chunk’s non-array path to pre-size the first chunk buffer based on a count hint, aiming to reduce intermediate Array.Resize growth while filling the first chunk.
Changes:
- Use
source.TryGetNonEnumeratedCount(out int countHint)to size the first buffer toMath.Min(size, countHint)when a positive hint is available. - Preserve the existing “start small” behavior (
Math.Min(size, 4)) when no count hint is available.
Show a summary per file
| File | Description |
|---|---|
| src/libraries/System.Linq/src/System/Linq/Chunk.cs | Pre-sizes the first chunk’s staging buffer using TryGetNonEnumeratedCount to avoid repeated growth reallocations in the first chunk for countable sources. |
Copilot's findings
- Files reviewed: 1/1 changed files
- Comments generated: 1
| // the array we'll yield. If the source's count is known up front, size the first chunk exactly | ||
| // (capped at the chunk size) so that filling it involves no growth/resizes. Otherwise start | ||
| // small to avoid significantly overallocating when the source has many fewer elements than the | ||
| // chunk size; the fill loop below still grows the array as needed. | ||
| int arraySize = source.TryGetNonEnumeratedCount(out int countHint) && countHint > 0 ? | ||
| Math.Min(size, countHint) : | ||
| Math.Min(size, 4); |
|
Closing as withdrawn (no-go). This revives a change LINQ maintainers already declined in #73134 (2022) absent evidence it matters in real applications, and it introduces an O(n)/locking Note This comment was created with GitHub Copilot. |
Status: Withdrawn (no-go). Not merging. Recording the rationale for future reference.
What was proposed
Pre-size the first
Enumerable.Chunkbuffer for countable non-array sources using a non-enumerating count hint (TryGetNonEnumeratedCount), i.e.Math.Min(size, count)instead of growing4→8→…→sizeviaArray.Resize. (Array sources are already special-cased and were untouched.)Why this is a no-go
Enumerable.Chunk()could reduce memory allocation without reducing its performance #115079 has no maintainer buy-in.TryGetNonEnumeratedCountcallsICollection<T>.Count/ non-genericICollection.Count, which is O(n) and/or locking for sources likeConcurrentStack<T>andConcurrentBag<T>. This turnssource.Chunk(n).First()from O(1) into O(n) and adds an extra full pass even for complete enumeration of such sources — the opposite of a performance win.size <= 4, the count hint can never beat the existingMath.Min(size, 4), so theCountcall is pure overhead.Outcome
No change to
main. Closing as withdrawn. The lazy/uncountable guardrail and correctness of the prototype were confirmed, but the maintainer-precedent and the expensive-Countregression make this not worth pursuing without (a) fresh maintainer appetite on #115079 and (b) a design that restricts the hint to provably cheap count providers plus real-world justification.Note
This pull request description was created with GitHub Copilot.