-
Notifications
You must be signed in to change notification settings - Fork 5.4k
Description
Background and Motivation
The existing CollectionsMarshal.AsSpan(List) API allows developers to use List for performant code as a Span, however it restricts modifications of it by having no way to change the Count of the List. Exposing an API in CollectionsMarshal for doing that would solve that and help to remove redundant array allocations and AddRange copies when adding data to a List that comes from an API that takes a Span (AddRange doesn't accept Spans so the buffers can't even be stack allocated).
Implementation details:
- Passing the current count would be an noop
- List version would be changed unless the count would be the same as the current one
- Passing a count bigger than the current capacity, the List would be resized
- Passing a count smaller than the current one would shrink the list and clear the data ONLY if it contains GC references (just like Clear does)
- Passing a count bigger than the current one would increase the count and it would NOT initialize the exposed data.
Proposed API
namespace System.Runtime.InteropServices
{
public static class CollectionsMarshal {
+ public static void SetCount<T>(List<T> list, int count); // alternative could be EnsureCount
}
}Example implementation:
public static void SetCount<T>(List<T> list, int count)
{
if (count < 0)
throw new ArgumentOutOfRangeException(); // would probably be a throw helper
if (count == list._size)
return;
list._version++; // would need to be made internal
if (count < list._size)
{
if (RuntimeHelpers.IsReferenceOrContainsReferences<T>())
Array.Clear(list._items, count, list._size - count);
list._size = count;
return;
}
if (count > list.Capacity)
list.Capacity = count;
list._size = count;
}Usage Examples
The API could be used for creating an extension method in place of the missing ReadOnlySpan AddRange:
public static void AddRange<T>(this List<T> list, ReadOnlySpan<T> span)
{
int oldCount = list.Count;
CollectionMarshal.SetCount(list, oldCount + span.Length);
span.CopyTo(CollectionMarshal.AsSpan(list).Slice(oldCount))
}Random.GetBytes is an example API that fills a Span with data here.
List<byte> list = new(16);
CollectionsMarshal.SetCount(list, 16);
Random.Shared.GetBytes(CollectionsMarshal.AsSpan(list));
// we have a count 16 list filled with random dataThis would be as valid, just a bit less performant:
List<byte> list = new();
CollectionsMarshal.SetCount(list, 16);
Random.Shared.GetBytes(CollectionsMarshal.AsSpan(list));
// we have a count 16 list filled with random dataWith this the end count would also be 16
List<byte> list = new() {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20 };
CollectionsMarshal.SetCount(list, 16);
Random.Shared.GetBytes(CollectionsMarshal.AsSpan(list));
// we have a count 16 list filled with random dataWith this the end count would also be 16 and we'd have the original content from before Clear after SetLength (not guaranteed, implementation detail)
List<byte> list = new() {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20 };
list.Clear();
CollectionsMarshal.SetCount(list, 16);
// accessing the list here would give { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 } since Clear wouldn't zero the data
Random.Shared.GetBytes(CollectionsMarshal.AsSpan(list));
// we have a count 16 list filled with random dataAlternative Designs
Currently the developer is required to do this, which requires an array allocation and a copy coming from AddRange:
List<byte> list = new(16);
list.AddRange(new byte[16]);The first example usage would look like this:
List<byte> list = new(16);
byte[] array = new byte[16];
RandomNumberGenerator.Fill(array);
list.AddRange(array);Risks
The API could expose non cleared memory for value types with no GC references (when the List was cleared or if Lists would use GetUninitalizedArray when the initial array was allocated unitialized).