meetings/working-groups/params-improvements/PI-2022-11-03.md
The second meeting of the params improvements working group was focused on the code generated for arguments to params ReadOnlySpan<T>.
The compiler may allocate a span on the stack when:
params argument or as a collection literal),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:
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:
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.
The compiler may reuse a span when:
params argument or as a collection literal),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.
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:
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());
Consider the following method with multiple calls to a params method:
// 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:
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
}