From 5a1c94536eacb3b2bb7b11c1183414a7bbdf48be Mon Sep 17 00:00:00 2001 From: Andrew J Said Date: Tue, 2 Jul 2024 18:23:06 +0100 Subject: [PATCH 1/6] Use SegmentedArrayBuilder in ToList --- .../src/System/Linq/SegmentedArrayBuilder.cs | 23 ++++++++++++++++++ .../src/System/Linq/ToCollection.cs | 24 ++++++++++++++++--- 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/src/libraries/System.Linq/src/System/Linq/SegmentedArrayBuilder.cs b/src/libraries/System.Linq/src/System/Linq/SegmentedArrayBuilder.cs index 0abaab4156bb8a..2f5ee31aaedb6e 100644 --- a/src/libraries/System.Linq/src/System/Linq/SegmentedArrayBuilder.cs +++ b/src/libraries/System.Linq/src/System/Linq/SegmentedArrayBuilder.cs @@ -5,6 +5,7 @@ using System.Diagnostics; using System.Linq; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; namespace System.Collections.Generic { @@ -251,6 +252,28 @@ public readonly T[] ToArray() return result; } + /// Creates a list containing all of the elements in the builder. + public readonly List ToList() + { + List result; + int count = Count; + + if (count != 0) + { + result = new List(count); + + CollectionsMarshal.SetCount(result, count); + Span span = CollectionsMarshal.AsSpan(result); + ToSpanInlined(span); + } + else + { + result = []; + } + + return result; + } + /// Creates an array containing all of the elements in the builder. /// The number of extra elements of room to allocate in the resulting array. public readonly T[] ToArray(int additionalLength) diff --git a/src/libraries/System.Linq/src/System/Linq/ToCollection.cs b/src/libraries/System.Linq/src/System/Linq/ToCollection.cs index 05e18b2382c8c6..8bda693a7a06f3 100644 --- a/src/libraries/System.Linq/src/System/Linq/ToCollection.cs +++ b/src/libraries/System.Linq/src/System/Linq/ToCollection.cs @@ -59,9 +59,9 @@ private static TSource[] ICollectionToArray(ICollection collec public static List ToList(this IEnumerable source) { - if (source is null) + if (source is ICollection collection) { - ThrowHelper.ThrowArgumentNullException(ExceptionArgument.source); + return new List(collection); } #if !OPTIMIZE_FOR_SIZE @@ -71,7 +71,25 @@ public static List ToList(this IEnumerable source) } #endif - return new List(source); + return EnumerableToList(source); + + [MethodImpl(MethodImplOptions.NoInlining)] // avoid large stack allocation impacting other paths + static List EnumerableToList(IEnumerable source) + { + if (source is null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.source); + } + + SegmentedArrayBuilder.ScratchBuffer scratch = default; + SegmentedArrayBuilder builder = new(scratch); + + builder.AddNonICollectionRangeInlined(source); + List result = builder.ToList(); + + builder.Dispose(); + return result; + } } /// From b082e7fbbf4ec24e06408ebef2fd86c667a53a60 Mon Sep 17 00:00:00 2001 From: Andrew J Said Date: Wed, 3 Jul 2024 00:04:26 +0100 Subject: [PATCH 2/6] Implement SegmentedArrayBuilder for Select iterators --- .../src/System/Linq/Select.SpeedOpt.cs | 36 ++++++++++++++----- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/src/libraries/System.Linq/src/System/Linq/Select.SpeedOpt.cs b/src/libraries/System.Linq/src/System/Linq/Select.SpeedOpt.cs index f491f1f0de015a..91c3b8a0d5eb13 100644 --- a/src/libraries/System.Linq/src/System/Linq/Select.SpeedOpt.cs +++ b/src/libraries/System.Linq/src/System/Linq/Select.SpeedOpt.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Diagnostics; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using static System.Linq.Utilities; @@ -31,15 +32,19 @@ public override TResult[] ToArray() public override List ToList() { - var list = new List(); + SegmentedArrayBuilder.ScratchBuffer scratch = default; + SegmentedArrayBuilder builder = new(scratch); Func selector = _selector; foreach (TSource item in _source) { - list.Add(selector(item)); + builder.Add(selector(item)); } - return list; + List result = builder.ToList(); + builder.Dispose(); + + return result; } public override int GetCount(bool onlyIfCheap) @@ -697,6 +702,25 @@ public override TResult[] ToArray() }; } + private List LazyToList() + { + Debug.Assert(_source.GetCount(onlyIfCheap: true) == -1); + + SegmentedArrayBuilder.ScratchBuffer scratch = default; + SegmentedArrayBuilder builder = new(scratch); + + Func selector = _selector; + foreach (TSource input in _source) + { + builder.Add(selector(input)); + } + + List result = builder.ToList(); + builder.Dispose(); + + return result; + } + public override List ToList() { int count = _source.GetCount(onlyIfCheap: true); @@ -704,11 +728,7 @@ public override List ToList() switch (count) { case -1: - list = new List(); - foreach (TSource input in _source) - { - list.Add(_selector(input)); - } + list = LazyToList(); break; case 0: list = new List(); From 4ff39643ecec0516c8b46b2d417bab7e6c51c87f Mon Sep 17 00:00:00 2001 From: Andrew J Said Date: Wed, 3 Jul 2024 00:20:13 +0100 Subject: [PATCH 3/6] Implement SegmentedArrayBuilder for OfType and SelectMany iterators --- .../System.Linq/src/System/Linq/OfType.SpeedOpt.cs | 10 +++++++--- .../src/System/Linq/SelectMany.SpeedOpt.cs | 12 ++++++++---- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/libraries/System.Linq/src/System/Linq/OfType.SpeedOpt.cs b/src/libraries/System.Linq/src/System/Linq/OfType.SpeedOpt.cs index 834fcdad006d7d..73983cf87fbde8 100644 --- a/src/libraries/System.Linq/src/System/Linq/OfType.SpeedOpt.cs +++ b/src/libraries/System.Linq/src/System/Linq/OfType.SpeedOpt.cs @@ -52,17 +52,21 @@ public override TResult[] ToArray() public override List ToList() { - var list = new List(); + SegmentedArrayBuilder.ScratchBuffer scratch = default; + SegmentedArrayBuilder builder = new(scratch); foreach (object? item in _source) { if (item is TResult castItem) { - list.Add(castItem); + builder.Add(castItem); } } - return list; + List result = builder.ToList(); + builder.Dispose(); + + return result; } public override TResult? TryGetFirst(out bool found) diff --git a/src/libraries/System.Linq/src/System/Linq/SelectMany.SpeedOpt.cs b/src/libraries/System.Linq/src/System/Linq/SelectMany.SpeedOpt.cs index ae0bf35ef8f1af..a521ab37295dbd 100644 --- a/src/libraries/System.Linq/src/System/Linq/SelectMany.SpeedOpt.cs +++ b/src/libraries/System.Linq/src/System/Linq/SelectMany.SpeedOpt.cs @@ -48,15 +48,19 @@ public override TResult[] ToArray() public override List ToList() { - var list = new List(); + SegmentedArrayBuilder.ScratchBuffer scratch = default; + SegmentedArrayBuilder builder = new(scratch); Func> selector = _selector; - foreach (TSource element in _source) + foreach (TSource item in _source) { - list.AddRange(selector(element)); + builder.AddRange(selector(item)); } - return list; + List result = builder.ToList(); + builder.Dispose(); + + return result; } } } From c49c2a0ee2cd905316b80825e2e779ea63d337d9 Mon Sep 17 00:00:00 2001 From: Andrew J Said Date: Wed, 3 Jul 2024 00:40:41 +0100 Subject: [PATCH 4/6] Implement SegmentedArrayBuilder for SkipTake and Where iterators --- .../src/System/Linq/SkipTake.SpeedOpt.cs | 13 ++++-- .../src/System/Linq/Where.SpeedOpt.cs | 40 +++++++++++++------ 2 files changed, 37 insertions(+), 16 deletions(-) diff --git a/src/libraries/System.Linq/src/System/Linq/SkipTake.SpeedOpt.cs b/src/libraries/System.Linq/src/System/Linq/SkipTake.SpeedOpt.cs index 1a49488b09fbb7..5cb53aa2997e06 100644 --- a/src/libraries/System.Linq/src/System/Linq/SkipTake.SpeedOpt.cs +++ b/src/libraries/System.Linq/src/System/Linq/SkipTake.SpeedOpt.cs @@ -491,8 +491,6 @@ public override TSource[] ToArray() public override List ToList() { - var list = new List(); - using (IEnumerator en = _source.GetEnumerator()) { if (SkipBeforeFirst(en) && en.MoveNext()) @@ -500,16 +498,23 @@ public override List ToList() int remaining = Limit - 1; // Max number of items left, not counting the current element. int comparand = HasLimit ? 0 : int.MinValue; // If we don't have an upper bound, have the comparison always return true. + SegmentedArrayBuilder.ScratchBuffer scratch = default; + SegmentedArrayBuilder builder = new(scratch); do { remaining--; - list.Add(en.Current); + builder.Add(en.Current); } while (remaining >= comparand && en.MoveNext()); + + List result = builder.ToList(); + builder.Dispose(); + + return result; } } - return list; + return []; } private bool SkipBeforeFirst(IEnumerator en) => SkipBefore(_minIndexInclusive, en); diff --git a/src/libraries/System.Linq/src/System/Linq/Where.SpeedOpt.cs b/src/libraries/System.Linq/src/System/Linq/Where.SpeedOpt.cs index 40a05db9abf8ff..ecc7be996b1e45 100644 --- a/src/libraries/System.Linq/src/System/Linq/Where.SpeedOpt.cs +++ b/src/libraries/System.Linq/src/System/Linq/Where.SpeedOpt.cs @@ -55,18 +55,22 @@ public override TSource[] ToArray() public override List ToList() { - var list = new List(); + SegmentedArrayBuilder.ScratchBuffer scratch = default; + SegmentedArrayBuilder builder = new(scratch); Func predicate = _predicate; foreach (TSource item in _source) { if (predicate(item)) { - list.Add(item); + builder.Add(item); } } - return list; + List result = builder.ToList(); + builder.Dispose(); + + return result; } public override TSource? TryGetFirst(out bool found) @@ -199,17 +203,21 @@ public static TSource[] ToArray(ReadOnlySpan source, Func ToList(ReadOnlySpan source, Func predicate) { - var list = new List(); + SegmentedArrayBuilder.ScratchBuffer scratch = default; + SegmentedArrayBuilder builder = new(scratch); foreach (TSource item in source) { if (predicate(item)) { - list.Add(item); + builder.Add(item); } } - return list; + List result = builder.ToList(); + builder.Dispose(); + + return result; } public override TSource? TryGetFirst(out bool found) @@ -398,17 +406,21 @@ public static TResult[] ToArray(ReadOnlySpan source, Func ToList(ReadOnlySpan source, Func predicate, Func selector) { - var list = new List(); + SegmentedArrayBuilder.ScratchBuffer scratch = default; + SegmentedArrayBuilder builder = new(scratch); foreach (TSource item in source) { if (predicate(item)) { - list.Add(selector(item)); + builder.Add(selector(item)); } } - return list; + List result = builder.ToList(); + builder.Dispose(); + + return result; } public override TResult? TryGetFirst(out bool found) => TryGetFirst(_source, _predicate, _selector, out found); @@ -538,7 +550,8 @@ public override TResult[] ToArray() public override List ToList() { - var list = new List(); + SegmentedArrayBuilder.ScratchBuffer scratch = default; + SegmentedArrayBuilder builder = new(scratch); Func predicate = _predicate; Func selector = _selector; @@ -546,11 +559,14 @@ public override List ToList() { if (predicate(item)) { - list.Add(selector(item)); + builder.Add(selector(item)); } } - return list; + List result = builder.ToList(); + builder.Dispose(); + + return result; } public override TResult? TryGetFirst(out bool found) From 730bbe8abb60e0f07b9366c6334e0d5d2def561a Mon Sep 17 00:00:00 2001 From: Andrew J Said Date: Tue, 16 Jul 2024 11:06:42 +0100 Subject: [PATCH 5/6] Revert change to ToList for non-specialized cases as requested in review --- .../src/System/Linq/SegmentedArrayBuilder.cs | 3 +-- .../src/System/Linq/ToCollection.cs | 24 +++---------------- 2 files changed, 4 insertions(+), 23 deletions(-) diff --git a/src/libraries/System.Linq/src/System/Linq/SegmentedArrayBuilder.cs b/src/libraries/System.Linq/src/System/Linq/SegmentedArrayBuilder.cs index 2f5ee31aaedb6e..e6b72d08885369 100644 --- a/src/libraries/System.Linq/src/System/Linq/SegmentedArrayBuilder.cs +++ b/src/libraries/System.Linq/src/System/Linq/SegmentedArrayBuilder.cs @@ -263,8 +263,7 @@ public readonly List ToList() result = new List(count); CollectionsMarshal.SetCount(result, count); - Span span = CollectionsMarshal.AsSpan(result); - ToSpanInlined(span); + ToSpanInlined(CollectionsMarshal.AsSpan(result)); } else { diff --git a/src/libraries/System.Linq/src/System/Linq/ToCollection.cs b/src/libraries/System.Linq/src/System/Linq/ToCollection.cs index 8bda693a7a06f3..05e18b2382c8c6 100644 --- a/src/libraries/System.Linq/src/System/Linq/ToCollection.cs +++ b/src/libraries/System.Linq/src/System/Linq/ToCollection.cs @@ -59,9 +59,9 @@ private static TSource[] ICollectionToArray(ICollection collec public static List ToList(this IEnumerable source) { - if (source is ICollection collection) + if (source is null) { - return new List(collection); + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.source); } #if !OPTIMIZE_FOR_SIZE @@ -71,25 +71,7 @@ public static List ToList(this IEnumerable source) } #endif - return EnumerableToList(source); - - [MethodImpl(MethodImplOptions.NoInlining)] // avoid large stack allocation impacting other paths - static List EnumerableToList(IEnumerable source) - { - if (source is null) - { - ThrowHelper.ThrowArgumentNullException(ExceptionArgument.source); - } - - SegmentedArrayBuilder.ScratchBuffer scratch = default; - SegmentedArrayBuilder builder = new(scratch); - - builder.AddNonICollectionRangeInlined(source); - List result = builder.ToList(); - - builder.Dispose(); - return result; - } + return new List(source); } /// From c3149b999af4f253eb9f5b89650116a4b2ba6683 Mon Sep 17 00:00:00 2001 From: Andrew J Said Date: Tue, 16 Jul 2024 11:53:27 +0100 Subject: [PATCH 6/6] Better naming for ToArray/ToList in Select enumerator when size is unknown --- .../System.Linq/src/System/Linq/Select.SpeedOpt.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/libraries/System.Linq/src/System/Linq/Select.SpeedOpt.cs b/src/libraries/System.Linq/src/System/Linq/Select.SpeedOpt.cs index 91c3b8a0d5eb13..98c686e514b68c 100644 --- a/src/libraries/System.Linq/src/System/Linq/Select.SpeedOpt.cs +++ b/src/libraries/System.Linq/src/System/Linq/Select.SpeedOpt.cs @@ -662,7 +662,7 @@ public override IEnumerable Select(Func s return sourceFound ? _selector(input!) : default!; } - private TResult[] LazyToArray() + private TResult[] ToArrayNoPresizing() { Debug.Assert(_source.GetCount(onlyIfCheap: true) == -1); @@ -696,13 +696,13 @@ public override TResult[] ToArray() int count = _source.GetCount(onlyIfCheap: true); return count switch { - -1 => LazyToArray(), + -1 => ToArrayNoPresizing(), 0 => [], _ => PreallocatingToArray(count), }; } - private List LazyToList() + private List ToListNoPresizing() { Debug.Assert(_source.GetCount(onlyIfCheap: true) == -1); @@ -728,7 +728,7 @@ public override List ToList() switch (count) { case -1: - list = LazyToList(); + list = ToListNoPresizing(); break; case 0: list = new List();