Skip to content

Latest commit

 

History

History
64 lines (45 loc) · 4.51 KB

File metadata and controls

64 lines (45 loc) · 4.51 KB

Collection literals working group meeting for August 10, 2023

Agenda

  • Determine behavior when target runtime does not support Inline-Arrays.

Discussion

The purpose of the meeting was to go over the decisions made in the last WG meeting (https://github.com/dotnet/csharplang/blob/main/meetings/working-groups/collection-literals/CL-2023-08-03.md) and consider their impact in the context of users targeting runtimes which do not support inline-arrays.

As a reminder, the final conclusion from that meeting was:

Working group goes with option 3. Collection expressions targeted to ref-struct types are always stack allocated.

Currently, the approach we are intending to take for targeting something like Span<T>/ReadOnlySpan<T> is to use the inline-arrays feature to synthesize an inline-array to place the data to, and then obtain a span from it. However, inline-arrays are only available on Net8.0 and up.

Further discussion on this topic raised concerns from several participants that this approach was too onerous in practice. Specifically, that a core use case in modern .NET is for users to multi-target their code. This multi-targeting appears in practice with users targeting frameworks like netstandard2.0 and netX.0, or just multiple versions of netX.0 (like net6.0/net7.0/net8.0/etc.). This raises a dilemma where users who are multi-targeting are then shut out from being able to use collection expressions with spans.

Stack allocating, though, is not a mandatory requirement. It is possible for the compiler to still generate code in these cases. Specifically, by allocating an array (on the heap) and storing it in the span. Note: this would not change the local-versus-global scope aspect of the span. Whether the span has local or global scope would use the same rules decided from the last working group meeting. It would just be the case that a local-scoped span would be stack-allocated on targets that supported inline-arrays, but heap allocated on targets that did not. This is necessary so that a user moving from a prior runtime (without inline-array support) to a current runtime would not find their scopes become more restrictive, certainly breaking user code that had come to depend on the global scope from before.

Vigorous debate about this followed, with concerns both about shutting people out from being able to use collection expressions in these real-world cases, while also not wanting usage of collection expressions to silently cause users to go over invisible performance cliffs. Ultimately, we concluded that shutting people off from using collection literals in multi-targeting scenarios was not acceptable. Rather, users should be in control and should be able to make an informed choice about what best strategy to take in these circumstances. To that end though, we felt strongly that users not run into a silent performance cliff. As such, our final determination was:

  1. No change in global/local scoping rules for spans and collection expresssions
  2. No change in rules around blittable collections assigned to `ReadOnlySpan`` (they remain global scoped, and will be read from the data segment of a dll)
  3. On target frameworks that support inline-arrays, we will use them and target the stack as the location where the data is actually stored.
  4. On target frameworks that do not support inline-arrays, we will fall back to allocating an array on the heap and having the span point at that array. We will also issue a warning in this case to make the user aware of the allocation.

The final two rules apply to the spans created as temporary storage for the collection builder pattern as well.

Examples:

ReadOnlySpan<int> A()
{
    ReadOnlySpan<int> x = [GetInt(), GetInt(), GetInt()];

    // Always illegal on any framework.  'x' has local scope.
    return x;
}

ReadOnlySpan<int> B()
{
    ReadOnlySpan<int> x = [1, 2, 3];

    // Always legal.  'x' has global scope due to being a ReadOnlySpan of constant blittable data.
    return x;
}

void C()
{
    // Stack allocated (using Inline-Array on Net8 and up)
    //
    // Heap allocated (using an int[] on Net7 and below).  Will warn in that event.
    ReadOnlySpan<X> x = [Get(), Get(), Get()];
}

void D()
{
    // Temporary ReadOnlySpan<int> is stack allocated (using Inline-Array on Net8 and up)
    //
    // Temporary ReadOnlySpan<int> is heap allocated (using an int[] on Net7 and below).  Will warn in that event.
    ImmutableList<X> x = [Get(), Get(), Get()];
}