csharplang
csharplang copied to clipboard
Champion "params Span<T>"
- [x] Proposal added
- [ ] Discussed in LDM
- [ ] Decision in LDM
- [ ] Finalized (done, rejected, inactive)
- [ ] Spec'ed
As suggested by @alrz in https://github.com/dotnet/csharplang/issues/1412#issuecomment-375839695, we could permit params Span<T> to implement params parameter-passing without any heap allocation. This could make the use of params methods much more efficient.
Proposal: https://github.com/dotnet/csharplang/blob/main/proposals/params-span.md
LDM history:
- https://github.com/dotnet/csharplang/blob/master/meetings/2018/LDM-2018-09-19.md#params-spant
- https://github.com/dotnet/csharplang/blob/main/meetings/2021/LDM-2021-11-03.md#params-spant
- https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-09-26.md#params-improvements
Previous discussion: https://github.com/dotnet/csharplang/issues/535
Similar issue which could be embraced by this proposal #1758
This would not work with the current CLR spec, as stackalloc'ed memory cannot contain references.
This would not work with the current CLR spec, as stackalloc'ed memory cannot contain references.
We could fallback to Span(T[]) in that case (or basically, anywhere stackalloc is forbidden).
This ones mentioned in the prior thread,
SomeType Foo(params int[] values); SomeType Foo(params Span<int> values);
As discussed before, we don't need to define any resolution precedence here, you can just move params to the span overload to make it preferred without incurring any breaking changes.
That being said, I propose to disallow such overloads because almost all invocations would be ambiguous and the overload resolution as-is wouldn't make it an error for "free" (in the expanded form).
... I propose to disallow such overloads ...
What kind of rule did you have in mind for that?
... I propose to disallow such overloads ...
I would prefer to handle this in the params expansion rules than in overload restrictions. Have it explicitly expand into a Span if available, otherwise into an array/enumerable.
This makes more sense to me because from a caller's standpoint just calling Foo(1, 2, 3), the params are "containerless" and I'd expect the compiler to expand into the most optimal choice. And importantly, it would also provide a simple upgrade path for current APIs taking arrays: supply the new API, recompile the code using that API, and everything gets faster without extra work.
@scalablecory The params expansion rule in https://github.com/dotnet/csharplang/blob/master/spec/expressions.md#applicable-function-member makes the underlying type of the params parameter (array or span) vanish for the purposes of overload resolution, so I think that doesn't work.
The expanded form is constructed by replacing the parameter array in the function member declaration with zero or more value parameters of the element type of the parameter array such that the number of arguments in the argument list
Amatches the total number of parameters. IfAhas fewer arguments than the number of fixed parameters in the function member declaration, the expanded form of the function member cannot be constructed and is thus not applicable.
@gafter
params parameters with identical underlying types would give you a conflicting overload error, regardless of it being array or span (e.g. int in the example above). This is applicable to params IEnumerable<T> as well.
@scalablecory
I would prefer to handle this in the params expansion rules than in overload restrictions. Have it explicitly expand into a Span if available, otherwise into an array/enumerable.
As I said, there's no need to have a preference (if at all possible), for instance, if you have
void Foo(params int[] values);
You can add an overload and move the params,
void Foo(params Span<int> values);
void Foo(int[] values);
This is still binary-compatible with existing code, and a recompilation would resolve all those invocations to the new overload.
Downside of this approach is that you must upgrade your compiler to be able to use such APIs. That doesn't mean that we need to bump up the lang version, we can continue to expand to span at the call-site even with prior lang versions.
This would be another great case for #1447 Proposal: Customisable overload resolution
Would the upcoming work on object stack allocation in CoreCLR (see https://github.com/dotnet/coreclr/pull/20251) make this feature less important?
@svick wow didn't know that's a thing. I think that would permit using stackalloced Span storage also with references?
Would the upcoming work on object stack allocation in CoreCLR (see dotnet/coreclr#20251) make this feature less important?
I should note that due to required escape analysis on stackalloced memory, this can't be applied to regular params arrays, if that's what you're implying.
@alrz Why not? The params array usually does not escape the called function, so escape analysis should figure out that the array can be stack allocated.
Stack alloc would be great for params array.
Expecially the following BCL methods would likely benefit significantly:
String.Concat (2 overloads)
String.Format (2 overloads)
String.Join (2 overloads)
String.Split (1 overloads)
String.Trim (1 overloads)
String.TrimEnd (1 overloads)
String.TrimStart (1 overloads)
StringBuilder.AppendFormat (2 overloads)
Task.WaitAll (1 overloads)
Task.WaitAny (1 overloads)
Task.WhenAll (2 overloads)
Task.WhenAny (2 overloads)
TextWriter.Write (4 overloads)
TextWriter.WriteLine (4 overloads)
Maybe the Jit can smart and only do stackalloc for arrays bellow a certain size, but it would definatelly be usefull.
The params array usually does not escape the called function.
@svick there's no restriction on the callee, it could freely leak out the array instance. That's not true of Span.
@alrz I don't follow - whether or not the function allows the array to escape and the object accordingly allowed to allocate on the stack, or not, would be deduced on a case-by-case basis. Although, personally, I agree; that decision should be reinforced by explicitly using a Span parameter.
Shouldn't this be a part of #179 ? I find it strange these are both championed, but in different boards and not cross-linked.
Upd: I just discovered #2302, which links to both. I assume all cases are considered together in the end.
Can't it just be:
SomeType Foo(params<int> values);
And C# treats it as:
SomeType Foo(params Span<int> values);?
Would this also allows us to reflectively invoke a method with reference of struct as argument without copying? I believe currently we cannot allocate an array of TypedReferences and send to reflection API.
OK maybe this is not related, because we still cannot convert ref structs to object or something.
This is great!
Cannot wait to see something like
public T Add<T>(params Span<T> values) where T : IMonoid<T>
This would not work with the current CLR spec, as stackalloc'ed memory cannot contain references.
We could fallback to
Span(T[])in that case (or basically, anywhere stackalloc is forbidden).
Since the parameter count is constant, another option would be to have the compiler generate a struct and transform this:
SomeClass.SomeMethodTakingParams(new object(), new object(), new object(), new object());
public static class SomeClass
{
public static void SomeMethodTakingParams(params Span<object> span) {}
}
into this:
SomeClass.SomeMethodTakingParams(new SomeName(new object(), new object(), new object(), new object()).AsSpan);
internal ref struct SomeName
{
public object o1;
public object o2;
public object o3;
public object o4;
public Span<object> AsSpan => MemoryMarshal.CreateSpan(ref o1, 4);
public SomeName(object o1, object o2, object o3, object o4)
{
this.o1 = o1;
this.o2 = o2;
this.o3 = o3;
this.o4 = o4;
}
}
public static class SomeClass
{
public static void SomeMethodTakingParams(Span<object> span) {}
}
@MichalPetryka
MemoryMarshal is unsafe.
using System.Runtime.InteropServices;
ref var obj = ref m();
n();
Console.WriteLine(obj.ToString()); // NullReference
ref object m()
{
SomeName span = new(new(), new(), new(), new());
return ref span.AsSpan[0]; // ref to stack is escaping
}
void n()
{
Span<byte> span = stackalloc byte[32]; // clear stack to zero
}
internal ref struct SomeName
{
public object o1;
public object o2;
public object o3;
public object o4;
public Span<object> AsSpan => MemoryMarshal.CreateSpan(ref o1, 4);
public SomeName(object o1, object o2, object o3, object o4)
{
this.o1 = o1;
this.o2 = o2;
this.o3 = o3;
this.o4 = o4;
}
}
@MichalPetryka
MemoryMarshalis unsafe.using System.Runtime.InteropServices; ref var obj = ref m(); n(); Console.WriteLine(obj.ToString()); // NullReference ref object m() { SomeName span = new(new(), new(), new(), new()); return ref span.AsSpan[0]; // ref to stack is escaping } void n() { Span<byte> span = stackalloc byte[32]; // clear stack to zero } internal ref struct SomeName { public object o1; public object o2; public object o3; public object o4; public Span<object> AsSpan => MemoryMarshal.CreateSpan(ref o1, 4); public SomeName(object o1, object o2, object o3, object o4) { this.o1 = o1; this.o2 = o2; this.o3 = o3; this.o4 = o4; } }
MemoryMarshal is as unsafe as stackalloc (a simplification ignoring things like stack cookies (which wouldn't matter here since the Span prevents writing out of bounds and you can't pin it because of managed types) and additional errors (which I mention in the next sentence) that could also be added here for params), using which was one of the base agreements on this proposal. As long as the called method won't return the passed Span (which the compiler already prevents for stackalloc and could be made an error for all params Span<T>), the compiler generated code can be ensured to be completely safe if it's generated like in my example. Your example doesn't prove anything since the compiler would never generate code like this.
And a question unrelated to the exact implementation, would params ReadOnlySpan<T> be also permitted?
Since having to do this would be quite annoying:
public static void A(params Span<int> s) => A((ReadOnlySpan<int>)s);
public static void A(ReadOnlySpan<int> s ) { /*some code*/ }
Curious, would this also be able to work on attribute constructors?
public sealed class Attr : Attribute
{
public Attr(params ReadOnlySpan<int> values);
}
// In usage:
[Attr(1, 2, 4)]
Obviously, the ctor would have to copy the values out to a heapable type, but it'd save on allocating the intermediate array. I'm simply putting it out there for consideration, because I can imagine it'd need a little bit of extra co-ordination to have it work.
@MichalPetryka
I'm of the opinion that it must support ReadOnlySpan<T> because anyone who uses this feature (including potential runtime APIs) should use that in the vast majority of cases, and devs should be guided that way by docs, examples, and potentially an analyzer.
I've never seen a params API that writes back into the array it's given, and even if there does exist one that does so for non-trivial purposes, that's probably 1 in a billion or something. So the vast majority of methods that need only read from the given span should encode that as part of their contract.
C# does not currently allow ReadOnlySpan<T> with or without params as a parameter of an attribute constructor that is called in metadata. I don't think such a feature should be part of this issue. It seems allowing ReadOnlySpan<T> in attributes can be implemented without allowing ReadOnlySpan<T> with params, and vice versa.
How should the data of the span be encoded in the attribute blob? In ECMA-335 section II.23.3 (Custom attributes), the encoding of each FixedArg depends on the type of the parameter in the constructor signature. A span parameter is not an SZARRAY in the signature so the encoding should have FieldOrPropType and Val for the best compatibility with existing metadata readers. The FieldOrPropType would be the same as for T[].
COMCustomAttribute::CreateCaObject could then build the array as usual and convert it to ReadOnlySpan<T> just before it calls the constructor. (A similar change would be needed for initialising a field or property of an attribute with a ReadOnlySpan<T>.) A later version of the runtime could optimise the array allocations to the stack without changing the metadata.
However, as C# does not even allow IEnumerable<T> as an attribute constructor parameter, I doubt supporting ReadOnlySpan<T> there would be worthwhile. Attribute types can have constructors for both T[] and ReadOnlySpan<T>, and the array allocation won't matter if the attribute instance is then cached.
And a question unrelated to the exact implementation, would params ReadOnlySpan<T> be also permitted?
Yes that is one of the types we would expand to allow with params.
Curious, would this also be able to work on attribute constructors?
Yes we would allow params of any form to be defined on attribute constructors. It's just another method and wouldn't be subject to any extra restrictions. However that particular constructor would not be usable when using the attribute as an annotation on methods, types, etc ... You would still need an array constructor overload for that.
However that particular constructor would not be usable when using the attribute as an annotation on methods, types, etc ... You would still need an array constructor overload for that.
Figures. I thought there may have been a way similar to this proposed method that could return a span pointing into an MDArray's object header.