Skip to content

Commit 04ae5cc

Browse files
authored
Merge pull request #1042 from Unity-Technologies/unity-master-fix-ienumerable-toarray
pull in fix for 1066693 from upstream
2 parents a098bd2 + f313961 commit 04ae5cc

File tree

2 files changed

+391
-1
lines changed

2 files changed

+391
-1
lines changed
Lines changed: 390 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,390 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using System.Diagnostics;
6+
using System.Runtime.CompilerServices;
7+
8+
namespace System.Collections.Generic
9+
{
10+
/// <summary>
11+
/// Represents a position within a <see cref="LargeArrayBuilder{T}"/>.
12+
/// </summary>
13+
[DebuggerDisplay("{DebuggerDisplay,nq}")]
14+
internal struct CopyPosition
15+
{
16+
/// <summary>
17+
/// Constructs a new <see cref="CopyPosition"/>.
18+
/// </summary>
19+
/// <param name="row">The index of the buffer to select.</param>
20+
/// <param name="column">The index within the buffer to select.</param>
21+
internal CopyPosition(int row, int column)
22+
{
23+
Debug.Assert(row >= 0);
24+
Debug.Assert(column >= 0);
25+
26+
Row = row;
27+
Column = column;
28+
}
29+
30+
/// <summary>
31+
/// Represents a position at the start of a <see cref="LargeArrayBuilder{T}"/>.
32+
/// </summary>
33+
public static CopyPosition Start => default(CopyPosition);
34+
35+
/// <summary>
36+
/// The index of the buffer to select.
37+
/// </summary>
38+
internal int Row { get; }
39+
40+
/// <summary>
41+
/// The index within the buffer to select.
42+
/// </summary>
43+
internal int Column { get; }
44+
45+
/// <summary>
46+
/// If this position is at the end of the current buffer, returns the position
47+
/// at the start of the next buffer. Otherwise, returns this position.
48+
/// </summary>
49+
/// <param name="endColumn">The length of the current buffer.</param>
50+
public CopyPosition Normalize(int endColumn)
51+
{
52+
Debug.Assert(Column <= endColumn);
53+
54+
return Column == endColumn ?
55+
new CopyPosition(Row + 1, 0) :
56+
this;
57+
}
58+
59+
/// <summary>
60+
/// Gets a string suitable for display in the debugger.
61+
/// </summary>
62+
private string DebuggerDisplay => $"[{Row}, {Column}]";
63+
}
64+
65+
/// <summary>
66+
/// Helper type for building dynamically-sized arrays while minimizing allocations and copying.
67+
/// </summary>
68+
/// <typeparam name="T">The element type.</typeparam>
69+
internal struct LargeArrayBuilder<T>
70+
{
71+
private const int StartingCapacity = 4;
72+
private const int ResizeLimit = 8;
73+
74+
private readonly int _maxCapacity; // The maximum capacity this builder can have.
75+
private T[] _first; // The first buffer we store items in. Resized until ResizeLimit.
76+
private ArrayBuilder<T[]> _buffers; // After ResizeLimit * 2, we store previous buffers we've filled out here.
77+
private T[] _current; // Current buffer we're reading into. If _count <= ResizeLimit, this is _first.
78+
private int _index; // Index into the current buffer.
79+
private int _count; // Count of all of the items in this builder.
80+
81+
/// <summary>
82+
/// Constructs a new builder.
83+
/// </summary>
84+
/// <param name="initialize">Pass <c>true</c>.</param>
85+
public LargeArrayBuilder(bool initialize)
86+
: this(maxCapacity: int.MaxValue)
87+
{
88+
// This is a workaround for C# not having parameterless struct constructors yet.
89+
// Once it gets them, replace this with a parameterless constructor.
90+
Debug.Assert(initialize);
91+
}
92+
93+
/// <summary>
94+
/// Constructs a new builder with the specified maximum capacity.
95+
/// </summary>
96+
/// <param name="maxCapacity">The maximum capacity this builder can have.</param>
97+
/// <remarks>
98+
/// Do not add more than <paramref name="maxCapacity"/> items to this builder.
99+
/// </remarks>
100+
public LargeArrayBuilder(int maxCapacity)
101+
: this()
102+
{
103+
Debug.Assert(maxCapacity >= 0);
104+
105+
_first = _current = Array.Empty<T>();
106+
_maxCapacity = maxCapacity;
107+
}
108+
109+
/// <summary>
110+
/// Gets the number of items added to the builder.
111+
/// </summary>
112+
public int Count => _count;
113+
114+
/// <summary>
115+
/// Adds an item to this builder.
116+
/// </summary>
117+
/// <param name="item">The item to add.</param>
118+
/// <remarks>
119+
/// Use <see cref="Add"/> if adding to the builder is a bottleneck for your use case.
120+
/// Otherwise, use <see cref="SlowAdd"/>.
121+
/// </remarks>
122+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
123+
public void Add(T item)
124+
{
125+
Debug.Assert(_maxCapacity > _count);
126+
127+
if (_index == _current.Length)
128+
{
129+
AllocateBuffer();
130+
}
131+
132+
_current[_index++] = item;
133+
_count++;
134+
}
135+
136+
/// <summary>
137+
/// Adds a range of items to this builder.
138+
/// </summary>
139+
/// <param name="items">The sequence to add.</param>
140+
/// <remarks>
141+
/// It is the caller's responsibility to ensure that adding <paramref name="items"/>
142+
/// does not cause the builder to exceed its maximum capacity.
143+
/// </remarks>
144+
public void AddRange(IEnumerable<T> items)
145+
{
146+
Debug.Assert(items != null);
147+
148+
using (IEnumerator<T> enumerator = items.GetEnumerator())
149+
{
150+
T[] destination = _current;
151+
int index = _index;
152+
153+
// Continuously read in items from the enumerator, updating _count
154+
// and _index when we run out of space.
155+
156+
while (enumerator.MoveNext())
157+
{
158+
if (index == destination.Length)
159+
{
160+
// No more space in this buffer. Resize.
161+
_count += index - _index;
162+
_index = index;
163+
AllocateBuffer();
164+
destination = _current;
165+
index = _index; // May have been reset to 0
166+
}
167+
168+
destination[index++] = enumerator.Current;
169+
}
170+
171+
// Final update to _count and _index.
172+
_count += index - _index;
173+
_index = index;
174+
}
175+
}
176+
177+
/// <summary>
178+
/// Copies the contents of this builder to the specified array.
179+
/// </summary>
180+
/// <param name="array">The destination array.</param>
181+
/// <param name="arrayIndex">The index in <see cref="array"/> to start copying to.</param>
182+
/// <param name="count">The number of items to copy.</param>
183+
public void CopyTo(T[] array, int arrayIndex, int count)
184+
{
185+
Debug.Assert(arrayIndex >= 0);
186+
Debug.Assert(count >= 0 && count <= Count);
187+
Debug.Assert(array?.Length - arrayIndex >= count);
188+
189+
for (int i = 0; count > 0; i++)
190+
{
191+
// Find the buffer we're copying from.
192+
T[] buffer = GetBuffer(index: i);
193+
194+
// Copy until we satisfy count, or we reach the end of the buffer.
195+
int toCopy = Math.Min(count, buffer.Length);
196+
Array.Copy(buffer, 0, array, arrayIndex, toCopy);
197+
198+
// Increment variables to that position.
199+
count -= toCopy;
200+
arrayIndex += toCopy;
201+
}
202+
}
203+
204+
/// <summary>
205+
/// Copies the contents of this builder to the specified array.
206+
/// </summary>
207+
/// <param name="position">The position in this builder to start copying from.</param>
208+
/// <param name="array">The destination array.</param>
209+
/// <param name="arrayIndex">The index in <see cref="array"/> to start copying to.</param>
210+
/// <param name="count">The number of items to copy.</param>
211+
/// <returns>The position in this builder that was copied up to.</returns>
212+
public CopyPosition CopyTo(CopyPosition position, T[] array, int arrayIndex, int count)
213+
{
214+
Debug.Assert(array != null);
215+
Debug.Assert(arrayIndex >= 0);
216+
Debug.Assert(count > 0 && count <= Count);
217+
Debug.Assert(array.Length - arrayIndex >= count);
218+
219+
// Go through each buffer, which contains one 'row' of items.
220+
// The index in each buffer is referred to as the 'column'.
221+
222+
/*
223+
* Visual representation:
224+
*
225+
* C0 C1 C2 .. C31 .. C63
226+
* R0: [0] [1] [2] .. [31]
227+
* R1: [32] [33] [34] .. [63]
228+
* R2: [64] [65] [66] .. [95] .. [127]
229+
*/
230+
231+
int row = position.Row;
232+
int column = position.Column;
233+
234+
T[] buffer = GetBuffer(row);
235+
int copied =
236+
#if __MonoCS__
237+
CopyToCore(buffer, column, array, arrayIndex, count);
238+
#else
239+
CopyToCore(buffer, column);
240+
#endif
241+
242+
if (count == 0)
243+
{
244+
return new CopyPosition(row, column + copied).Normalize(buffer.Length);
245+
}
246+
247+
do
248+
{
249+
buffer = GetBuffer(++row);
250+
copied =
251+
#if __MonoCS__
252+
CopyToCore(buffer, 0, array, arrayIndex, count);
253+
#else
254+
CopyToCore(buffer, 0);
255+
#endif
256+
} while (count > 0);
257+
258+
return new CopyPosition(row, copied).Normalize(buffer.Length);
259+
260+
#if __MonoCS__
261+
}
262+
263+
static int CopyToCore(T[] sourceBuffer, int sourceIndex, T[] array, int arrayIndex, int count)
264+
#else
265+
int CopyToCore(T[] sourceBuffer, int sourceIndex)
266+
#endif
267+
{
268+
Debug.Assert(sourceBuffer.Length > sourceIndex);
269+
270+
// Copy until we satisfy `count` or reach the end of the current buffer.
271+
int copyCount = Math.Min(sourceBuffer.Length - sourceIndex, count);
272+
Array.Copy(sourceBuffer, sourceIndex, array, arrayIndex, copyCount);
273+
274+
arrayIndex += copyCount;
275+
count -= copyCount;
276+
277+
return copyCount;
278+
}
279+
280+
#if !__MonoCS__
281+
}
282+
#endif
283+
284+
/// <summary>
285+
/// Retrieves the buffer at the specified index.
286+
/// </summary>
287+
/// <param name="index">The index of the buffer.</param>
288+
public T[] GetBuffer(int index)
289+
{
290+
Debug.Assert(index >= 0 && index < _buffers.Count + 2);
291+
292+
return index == 0 ? _first :
293+
index <= _buffers.Count ? _buffers[index - 1] :
294+
_current;
295+
}
296+
297+
/// <summary>
298+
/// Adds an item to this builder.
299+
/// </summary>
300+
/// <param name="item">The item to add.</param>
301+
/// <remarks>
302+
/// Use <see cref="Add"/> if adding to the builder is a bottleneck for your use case.
303+
/// Otherwise, use <see cref="SlowAdd"/>.
304+
/// </remarks>
305+
[MethodImpl(MethodImplOptions.NoInlining)]
306+
public void SlowAdd(T item) => Add(item);
307+
308+
/// <summary>
309+
/// Creates an array from the contents of this builder.
310+
/// </summary>
311+
public T[] ToArray()
312+
{
313+
if (TryMove(out T[] array))
314+
{
315+
// No resizing to do.
316+
return array;
317+
}
318+
319+
array = new T[_count];
320+
CopyTo(array, 0, _count);
321+
return array;
322+
}
323+
324+
/// <summary>
325+
/// Attempts to transfer this builder into an array without copying.
326+
/// </summary>
327+
/// <param name="array">The transferred array, if the operation succeeded.</param>
328+
/// <returns><c>true</c> if the operation succeeded; otherwise, <c>false</c>.</returns>
329+
public bool TryMove(out T[] array)
330+
{
331+
array = _first;
332+
return _count == _first.Length;
333+
}
334+
335+
private void AllocateBuffer()
336+
{
337+
// - On the first few adds, simply resize _first.
338+
// - When we pass ResizeLimit, allocate ResizeLimit elements for _current
339+
// and start reading into _current. Set _index to 0.
340+
// - When _current runs out of space, add it to _buffers and repeat the
341+
// above step, except with _current.Length * 2.
342+
// - Make sure we never pass _maxCapacity in all of the above steps.
343+
344+
Debug.Assert((uint)_maxCapacity > (uint)_count);
345+
Debug.Assert(_index == _current.Length, $"{nameof(AllocateBuffer)} was called, but there's more space.");
346+
347+
// If _count is int.MinValue, we want to go down the other path which will raise an exception.
348+
if ((uint)_count < (uint)ResizeLimit)
349+
{
350+
// We haven't passed ResizeLimit. Resize _first, copying over the previous items.
351+
Debug.Assert(_current == _first && _count == _first.Length);
352+
353+
int nextCapacity = Math.Min(_count == 0 ? StartingCapacity : _count * 2, _maxCapacity);
354+
355+
_current = new T[nextCapacity];
356+
Array.Copy(_first, 0, _current, 0, _count);
357+
_first = _current;
358+
}
359+
else
360+
{
361+
Debug.Assert(_maxCapacity > ResizeLimit);
362+
Debug.Assert(_count == ResizeLimit ^ _current != _first);
363+
364+
int nextCapacity;
365+
if (_count == ResizeLimit)
366+
{
367+
nextCapacity = ResizeLimit;
368+
}
369+
else
370+
{
371+
// Example scenario: Let's say _count == 64.
372+
// Then our buffers look like this: | 8 | 8 | 16 | 32 |
373+
// As you can see, our count will be just double the last buffer.
374+
// Now, say _maxCapacity is 100. We will find the right amount to allocate by
375+
// doing min(64, 100 - 64). The lhs represents double the last buffer,
376+
// the rhs the limit minus the amount we've already allocated.
377+
378+
Debug.Assert(_count >= ResizeLimit * 2);
379+
Debug.Assert(_count == _current.Length * 2);
380+
381+
_buffers.Add(_current);
382+
nextCapacity = Math.Min(_count, _maxCapacity - _count);
383+
}
384+
385+
_current = new T[nextCapacity];
386+
_index = 0;
387+
}
388+
}
389+
}
390+
}

mcs/class/System.Core/common_System.Core.dll.sources

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -278,7 +278,7 @@ System.Security.Cryptography/SHA512CryptoServiceProvider.cs
278278
../../../external/corefx/src/Common/src/System/Collections/Generic/ArrayBuilder.cs
279279
../../../external/corefx/src/Common/src/System/Collections/Generic/EnumerableHelpers.cs
280280
../../../external/corefx/src/Common/src/System/Collections/Generic/EnumerableHelpers.Linq.cs
281-
../../../external/corefx/src/Common/src/System/Collections/Generic/LargeArrayBuilder.cs
281+
../../../external/corefx-bugfix/src/Common/src/System/Collections/Generic/LargeArrayBuilder.cs
282282
../../../external/corefx/src/Common/src/System/Collections/Generic/SparseArrayBuilder.cs
283283

284284
../../../external/corefx/src/System.Linq.Expressions/src/System/Dynamic/Utils/CacheDict.cs

0 commit comments

Comments
 (0)