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

Commit c8155b6

Browse files
committed
Merge pull request #1955 from stephentoub/sortedset_perf
Improve SortedSet performance
2 parents b4d3960 + 6337e28 commit c8155b6

File tree

5 files changed

+118
-65
lines changed

5 files changed

+118
-65
lines changed
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
// Copyright (c) Microsoft. All rights reserved.
2+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
3+
4+
namespace System.Collections.Generic
5+
{
6+
/// <summary>Internal helper functions for working with enumerables.</summary>
7+
internal static class EnumerableHelpers
8+
{
9+
/// <summary>Converts an enumerable to an array using the same logic as does List{T}.</summary>
10+
/// <param name="length">The number of items stored in the resulting array, 0-indexed.</param>
11+
/// <returns>
12+
/// The resulting array. The length of the array may be greater than <paramref name="length"/>,
13+
/// which is the actual number of elements in the array.
14+
/// </returns>
15+
internal static T[] ToArray<T>(IEnumerable<T> source, out int length)
16+
{
17+
T[] arr;
18+
int count = 0;
19+
20+
ICollection<T> ic = source as ICollection<T>;
21+
if (ic != null)
22+
{
23+
count = ic.Count;
24+
if (count == 0)
25+
{
26+
arr = Array.Empty<T>();
27+
}
28+
else
29+
{
30+
// Allocate an array of the desired size, then copy the elements into it. Note that this has the same
31+
// issue regarding concurrency as other existing collections like List<T>. If the collection size
32+
// concurrently changes between the array allocation and the CopyTo, we could end up either getting an
33+
// exception from overrunning the array (if the size went up) or we could end up not filling as many
34+
// items as 'count' suggests (if the size went down). This is only an issue for concurrent collections
35+
// that implement ICollection<T>, which as of .NET 4.6 is just ConcurrentDictionary<TKey, TValue>.
36+
arr = new T[count];
37+
ic.CopyTo(arr, 0);
38+
}
39+
}
40+
else
41+
{
42+
arr = Array.Empty<T>();
43+
foreach (var item in source)
44+
{
45+
if (count == arr.Length)
46+
{
47+
// MaxArrayLength is defined in Array.MaxArrayLength and in gchelpers in CoreCLR.
48+
// It represents the maximum number of elements that can be in an array where
49+
// the size of the element is greater than one byte; a separate, slightly larger constant,
50+
// is used when the size of the element is one.
51+
const int MaxArrayLength = 0x7FEFFFFF;
52+
53+
// This is the same growth logic as in List<T>:
54+
// If the array is currently empty, we make it a default size. Otherwise, we attempt to
55+
// double the size of the array. Doubling will overflow once the size of the array reaches
56+
// 2^30, since doubling to 2^31 is 1 larger than Int32.MaxValue. In that case, we instead
57+
// constrain the length to be MaxArrayLength (this overflow check works because of of the
58+
// cast to uint). Because a slightly larger constant is used when T is one byte in size, we
59+
// could then end up in a situation where arr.Length is MaxArrayLength or slightly larger, such
60+
// that we constrain newLength to be MaxArrayLength but the needed number of elements is actually
61+
// larger than that. For that case, we then ensure that the newLength is large enough to hold
62+
// the desired capacity. This does mean that in the very rare case where we've grown to such a
63+
// large size, each new element added after MaxArrayLength will end up doing a resize.
64+
const int DefaultCapacity = 4;
65+
int newLength = count == 0 ? DefaultCapacity : count * 2;
66+
if ((uint)newLength > MaxArrayLength)
67+
{
68+
newLength = MaxArrayLength;
69+
}
70+
if (newLength < count + 1)
71+
{
72+
newLength = count + 1;
73+
}
74+
75+
arr = ArrayT<T>.Resize(arr, newLength, count);
76+
}
77+
arr[count++] = item;
78+
}
79+
}
80+
81+
length = count;
82+
return arr;
83+
}
84+
}
85+
}

src/System.Collections/src/System.Collections.csproj

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,14 @@
4242
<Compile Include="System\Collections\StructuralComparisons.cs" />
4343
</ItemGroup>
4444
<ItemGroup>
45+
<Compile Include="$(CommonPath)\System\ArrayT.cs">
46+
<Link>Common\System\ArrayT.cs</Link>
47+
</Compile>
4548
<Compile Include="$(CommonPath)\System\Collections\HashHelpers.cs">
4649
<Link>Common\System\Collections\HashHelpers.cs</Link>
4750
</Compile>
48-
<Compile Include="$(CommonPath)\System\ArrayT.cs">
49-
<Link>Common\System\ArrayT.cs</Link>
51+
<Compile Include="$(CommonPath)\System\Collections\Generic\EnumerableHelpers.cs">
52+
<Link>Common\System\Collections\Generic\EnumerableHelpers.cs</Link>
5053
</Compile>
5154
</ItemGroup>
5255
<ItemGroup>

src/System.Collections/src/System/Collections/Generic/Queue.cs

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -32,14 +32,13 @@ public class Queue<T> : IEnumerable<T>,
3232
private const int MinimumGrow = 4;
3333
private const int GrowFactor = 200; // double each time
3434
private const int DefaultCapacity = 4;
35-
private static T[] s_emptyArray = Array.Empty<T>();
3635

3736
// Creates a queue with room for capacity objects. The default initial
3837
// capacity and grow factor are used.
3938
/// <include file='doc\Queue.uex' path='docs/doc[@for="Queue.Queue"]/*' />
4039
public Queue()
4140
{
42-
_array = s_emptyArray;
41+
_array = Array.Empty<T>();
4342
}
4443

4544
// Creates a queue with room for capacity objects. The default grow factor
@@ -50,11 +49,7 @@ public Queue(int capacity)
5049
{
5150
if (capacity < 0)
5251
throw new ArgumentOutOfRangeException("capacity", SR.ArgumentOutOfRange_NeedNonNegNumRequired);
53-
5452
_array = new T[capacity];
55-
_head = 0;
56-
_tail = 0;
57-
_size = 0;
5853
}
5954

6055
// Fills a Queue with the elements of an ICollection. Uses the enumerator
@@ -67,8 +62,6 @@ public Queue(IEnumerable<T> collection)
6762
throw new ArgumentNullException("collection");
6863

6964
_array = new T[DefaultCapacity];
70-
_size = 0;
71-
_version = 0;
7265

7366
using (IEnumerator<T> en = collection.GetEnumerator())
7467
{

src/System.Collections/src/System/Collections/Generic/SortedSet.cs

Lines changed: 25 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -95,13 +95,9 @@ public SortedSet(IEnumerable<T> collection, IComparer<T> comparer)
9595
//breadth first traversal to recreate nodes
9696
if (baseSortedSet.Count == 0)
9797
{
98-
_count = 0;
99-
_version = 0;
100-
_root = null;
10198
return;
10299
}
103100

104-
105101
//pre order way to replicate nodes
106102
Stack<Node> theirStack = new Stack<SortedSet<T>.Node>(2 * log2(baseSortedSet.Count) + 2);
107103
Stack<Node> myStack = new Stack<SortedSet<T>.Node>(2 * log2(baseSortedSet.Count) + 2);
@@ -138,23 +134,27 @@ public SortedSet(IEnumerable<T> collection, IComparer<T> comparer)
138134
}
139135
}
140136
_count = baseSortedSet._count;
141-
_version = 0;
142137
}
143138
else
144-
{ //As it stands, you're doing an NlogN sort of the collection
145-
List<T> els = new List<T>(collection);
146-
els.Sort(_comparer);
147-
for (int i = 1; i < els.Count; i++)
139+
{
140+
int count;
141+
T[] els = EnumerableHelpers.ToArray(collection, out count);
142+
if (count > 0)
148143
{
149-
if (comparer.Compare(els[i], els[i - 1]) == 0)
144+
Array.Sort(els, 0, count, _comparer);
145+
int index = 1;
146+
for (int i = 1; i < count; i++)
150147
{
151-
els.RemoveAt(i);
152-
i--;
148+
if (comparer.Compare(els[i], els[i - 1]) != 0)
149+
{
150+
els[index++] = els[i];
151+
}
153152
}
153+
count = index;
154+
155+
_root = ConstructRootFromSortedArray(els, 0, count - 1, null);
156+
_count = count;
154157
}
155-
_root = ConstructRootFromSortedArray(els.ToArray(), 0, els.Count - 1, null);
156-
_count = els.Count;
157-
_version = 0;
158158
}
159159
}
160160

@@ -257,25 +257,24 @@ internal virtual bool BreadthFirstTreeWalk(TreeWalkPredicate<T> action)
257257
return true;
258258
}
259259

260-
List<Node> processQueue = new List<Node>();
261-
processQueue.Add(_root);
260+
Queue<Node> processQueue = new Queue<Node>();
261+
processQueue.Enqueue(_root);
262262
Node current;
263263

264264
while (processQueue.Count != 0)
265265
{
266-
current = processQueue[0];
267-
processQueue.RemoveAt(0);
266+
current = processQueue.Dequeue();
268267
if (!action(current))
269268
{
270269
return false;
271270
}
272271
if (current.Left != null)
273272
{
274-
processQueue.Add(current.Left);
273+
processQueue.Enqueue(current.Left);
275274
}
276275
if (current.Right != null)
277276
{
278-
processQueue.Add(current.Right);
277+
processQueue.Enqueue(current.Right);
279278
}
280279
}
281280
return true;
@@ -2084,25 +2083,24 @@ internal override bool BreadthFirstTreeWalk(TreeWalkPredicate<T> action)
20842083
return true;
20852084
}
20862085

2087-
List<Node> processQueue = new List<Node>();
2088-
processQueue.Add(_root);
2086+
Queue<Node> processQueue = new Queue<Node>();
2087+
processQueue.Enqueue(_root);
20892088
Node current;
20902089

20912090
while (processQueue.Count != 0)
20922091
{
2093-
current = processQueue[0];
2094-
processQueue.RemoveAt(0);
2092+
current = processQueue.Dequeue();
20952093
if (IsWithinRange(current.Item) && !action(current))
20962094
{
20972095
return false;
20982096
}
20992097
if (current.Left != null && (!_lBoundActive || Comparer.Compare(_min, current.Item) < 0))
21002098
{
2101-
processQueue.Add(current.Left);
2099+
processQueue.Enqueue(current.Left);
21022100
}
21032101
if (current.Right != null && (!_uBoundActive || Comparer.Compare(_max, current.Item) > 0))
21042102
{
2105-
processQueue.Add(current.Right);
2103+
processQueue.Enqueue(current.Right);
21062104
}
21072105
}
21082106
return true;

src/System.Collections/src/System/Collections/Generic/Stack.cs

Lines changed: 2 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,11 @@ public class Stack<T> : IEnumerable<T>,
2929
private Object _syncRoot;
3030

3131
private const int DefaultCapacity = 4;
32-
private static T[] s_emptyArray = Array.Empty<T>();
3332

3433
/// <include file='doc\Stack.uex' path='docs/doc[@for="Stack.Stack"]/*' />
3534
public Stack()
3635
{
37-
_array = s_emptyArray;
38-
_size = 0;
39-
_version = 0;
36+
_array = Array.Empty<T>();
4037
}
4138

4239
// Create a stack with a specific initial capacity. The initial capacity
@@ -47,8 +44,6 @@ public Stack(int capacity)
4744
if (capacity < 0)
4845
throw new ArgumentOutOfRangeException("capacity", SR.ArgumentOutOfRange_NeedNonNegNumRequired);
4946
_array = new T[capacity];
50-
_size = 0;
51-
_version = 0;
5247
}
5348

5449
// Fills a Stack with the contents of a particular collection. The items are
@@ -59,28 +54,7 @@ public Stack(IEnumerable<T> collection)
5954
{
6055
if (collection == null)
6156
throw new ArgumentNullException("collection");
62-
63-
ICollection<T> c = collection as ICollection<T>;
64-
if (c != null)
65-
{
66-
int count = c.Count;
67-
_array = new T[count];
68-
c.CopyTo(_array, 0);
69-
_size = count;
70-
}
71-
else
72-
{
73-
_size = 0;
74-
_array = new T[DefaultCapacity];
75-
76-
using (IEnumerator<T> en = collection.GetEnumerator())
77-
{
78-
while (en.MoveNext())
79-
{
80-
Push(en.Current);
81-
}
82-
}
83-
}
57+
_array = EnumerableHelpers.ToArray(collection, out _size);
8458
}
8559

8660
/// <include file='doc\Stack.uex' path='docs/doc[@for="Stack.Count"]/*' />

0 commit comments

Comments
 (0)