Skip to content

Latest commit

 

History

History
91 lines (63 loc) · 5.29 KB

File metadata and controls

91 lines (63 loc) · 5.29 KB

Collection literals working group meeting for October 21st, 2022

This is the third meeting of the collection literals working group. The proposal is currently located at #5354.

Agenda

  • Span<T> and stack allocation
  • Preliminary review of collection literal speclet

Supporting Span<T> (and ReadOnlySpan<T>) is considered very important for completeness for the feature. Stack allocation of data (using stackalloc) is also considered highly desirable for the ability to avoid incurring a GC cost, including avoiding overhead when the literal itself isn't observed but only its elements are.

However, stack allocation is very dangerous as well. It can trivially cause stack explosions when not used carefully.

It turns out this space is being heavily examined in the params span proposal. We strongly believe that we would want the following to be equivalent:

void M(params ReadOnlySpan<int> values);
M(1, 2, 3, 4);   // as params span
M([1, 2, 3, 4]); // as direct span

As such, our position is to align with whatever the params span working group comes up with for using spans and stackalloc. Currently, that spec says that the span:

will result in an array T[] created on the stack if the params array is within limits (if any) set by the compiler. Otherwise the array will be allocated on the heap.

Following the same approach for collection literals would then always allow a literal to construct a Span<T>, albeit sometimes using stack allocation, and sometimes heap allocation. We know that there will be some users and some use cases where this is unpalatable. For example, code which wishes to use a Span<T> knowing that it will only stack-allocate. To that end, we expect the compiler to issue hidden diagnostics (a capability already supported today) when such a collection literal might initialize a span using a heap allocation. Concerned users would then enable a warning or error for that diagnostic, and could block any cases which the compiler doesn't guarantee will be on the stack.

Importantly, we do not anticipate that stack allocating means a direct translation of a collection literal expression to stackalloc. For example, given:

foreach (...)
{
   Span<T> values = [GetA(), GetB(), GetC()];
   // do things with values
}

a simplistic translation to:

foreach (...)
{
   Span<T> values = stackalloc T[3];

would be undesirable. This would grow the stack on each iteration of the loop, easily leading to a blowup at runtime. The compiler is allowed to translate that using stackalloc however it wants, as long as the Span meaning stays the same and span-safety is maintained. For example, it can translate the above to:

Span<int> __buffer = stackalloc int[3];
foreach (...)
{
    __buffer[0] = GetA();
    __buffer[1] = GetB();
    __buffer[2] = GetC();
    Span<int> values = __buffer;
    // do things with span.  It would likely be appropriate to clear __buffer when the scope ends.
}

This approach ensures that the stack does not grow in an unbounded fashion, though it may not be possible in all collection-literal cases. This would incur a cost even if the loop was never taken. But it feels absolutely more desirable than incurring growing for the much more common cases where the loop iterates multiple times.

If the compiler decides to allocate on the heap, the translation for Span<T> is simply:

T[] __array = [...]; // using existing rules
Span<T> __result = __array;

A preliminary review of the spec was performed with another LDM member. The purpose of the review was to get an early read on how the spec is shaping up, and how the decisions of the working group feel against the backdrop of the philosophy and history of C#. Reviews around the intuitions behind constructibility, convertibility, the type system, etc. were performed, including whether the concepts matched C# so far and whether the descriptions so far could easily translate into the actual rules needed for the specification. The review was highly positive, with understanding and approval of the decisions of the working group so far, and of what collection literals both aim to support and not support.

An entire topic was discovered that we have not considered so far: how collection literals operate with generic type inference. Specifically, the feature does not currently support:

void M<T>(T[] values);
M([1, 2, 3]);

However, there was strong agreement that this should work and that T should be inferrable to int here. We believe the rules to support this should not be too difficult, but it certainly gets interesting with more complex cases like the following:

void M<T>(ImmutableArray<T> values);
M([1, 2, 3]);

For inference, we would likely need to see that ImmutableArray<T> was constructible through the init void Construct(T[] values) method. Inference would then have to continue with T[] and [1, 2, 3]. That base case of inference would find that T was int which would flow out all the way to the top level inference.

This topic has been added to the agenda for the working group to work through.