Back to Csharplang

Params Improvements Working Group Meeting for November 3, 2022

meetings/working-groups/params-improvements/PI-2022-11-03.md

latest5.9 KB
Original Source

Params Improvements Working Group Meeting for November 3, 2022

The second meeting of the params improvements working group was focused on the code generated for arguments to params ReadOnlySpan<T>.

Span allocation

The compiler may allocate a span on the stack when:

  • the span is implicitly allocated (as a params argument or as a collection literal),
  • the span length is known at compile time, and
  • the compiler can determine through escape analysis that no references to the span escape the containing method.

For arguments to a params ReadOnlySpan<T> parameter (which is implicitly scoped), the conditions above are satisfied.

We have a request with the runtime team to support fixed size arrays of managed types, at least for fields. If we have fixed size array fields, we can define struct types with inline arrays and use locals for stack allocated arrays.

For example, consider a FixedSizeArray2 type defined below which includes an inline 2 element array:

csharp
struct FixedSizeArray2<T>
{
    public T[2] Array; // pseudo-code for inline array
}

With that type, a call to WriteLine("{0}, {1}", x, y) could be emitted as:

csharp
var _tmp1 = new FixedSizeArray2<object>();
_tmp1.Array[0] = x;
_tmp1.Array[1] = y;
var _tmp2 = new ReadOnlySpan<object>(_tmp.Array);

// WriteLine(string format, params ReadOnlySpan<object> args);
WriteLine("{0}, {1}", _tmp2);

Ideally the BCL will provide types such as FixedSizeArray1, FixedSizeArray2, etc. for a limited number of array lengths. And if the compilation requires spans for other argument lengths, the compiler will generate and emit the additional types.

The number of arguments passed to a params parameter is not considered when determining whether to implicitly allocate the span on the stack. To avoid implicit stack allocation at a call site, the calling code should allocate the array explicitly with new[] { ... }. We believe scenarios where stack allocation regardless of argument length becomes an issue are unlikely, and should be easy to work around, but we can adjust if necessary.

Span reuse

The compiler may reuse a span when:

  • the span is implicitly allocated (as a params argument or as a collection literal),
  • the span length is known at compile time,
  • the compiler can determine through escape analysis that no references to the span escape the containing method, and
  • the compiler can determine there are no reachable aliases to the span in user code.

For arguments to a params ReadOnlySpan<T> parameter (which is implicitly scoped), the conditions above are satisfied.

The compiler will reuse a single span across all calls to params ReadOnlySpan<T> methods where the span element types are considered identical by the runtime.

The length of the reused span will be the length of the longest params argument list from all uses. (The actual span argument passed to a params method will be a slice of the reused span, with the expected length for the call.)

Reuse may be across distinct call sites or repeated calls from the same call site. Reuse is per thread of execution and within the same method only. No reuse across lambda expressions and the containing method. Reusing spans across local functions and the containing method is possible if the local function is not used as a delegate, although the implementation cost of such an optimization may outweigh the benefit.

When exiting a scope, the compiler will ensure that no implicitly allocated span holds references from the scope.

To opt out of reuse at a call site, the calling code should allocate the span explicitly.

Should we only reuse spans that are allocated on the stack? If we also allow reuse of heap allocated buffers, that will require completely different code gen for managing allocations.

Collection literals

We will support stack allocation and reuse of spans for collection literals.

For collection literals that include a spread operator, the length of the resulting span is not known at compile time. For those cases, we will choose a maximum length for stack allocation and generate code that falls back to heap allocation at runtime if that length is exceeded. As per the collection literals working group, a hidden diagnostic will be reported in cases where stack allocation may not be possible at runtime.

For instance, the expression (ReadOnlySpan<int>)[..e] could be emitted as:

csharp
var _tmp1 = new FixedSizeArray8<int>();
ReadOnlySpan<int> _tmp2;
if (Enumerable.TryGetNonEnumeratedCount(e, out int n) && n <= 8)
{
    int i = 0;
    foreach (var item in e)
    {
        _tmp1.Array[i++] = item;
        if (i == n) break;
    }
    _tmp2 = new ReadOnlySpan<int>(_tmp1.Array, 0, n);
}
else
    _tmp2 = new ReadOnlySpan<int>(e.ToArray());

Example

Consider the following method with multiple calls to a params method:

csharp
// static void WriteLine(string format, params ReadOnlySpan<object> args);

static void WriteDictionary<K, V>(Dictionary<K, V> dictionary)
{
    WriteLine("Dictionary");

    foreach (var (k, v) in dictionary)
        WriteLine("{0}, {1}", k, v);

    WriteLine("Count = {0}", dictionary.Count);
}

The method could be lowered to:

csharp
static void WriteDictionary<K, V>(Dictionary<K, V> dictionary)
{
    FixedSizeArray2 _tmp1 = new FixedSizeArray2();

    WriteLine("Dictionary",
        new ReadOnlySpan<object>(Array.Empty<object>()); // no reuse

    foreach (var (k, v) in dictionary)
    {
        _tmp1.Array[0] = k;
        _tmp1.Array[1] = v;
        WriteLine("{0}, {1}",
            new ReadOnlySpan(_tmp1.Array)); // reuse
        Array.Clear(_tmp1.Array);           // clear
    }

    _tmp1.Array[0] = dictionary.Count;
    WriteLine("Count = {0}",
        new ReadOnlySpan(_tmp1.Array, 0, 1)); // reuse
}