csharplang icon indicating copy to clipboard operation
csharplang copied to clipboard

Champion "params Span<T>"

Open gafter opened this issue 7 years ago • 53 comments

  • [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

gafter avatar Jul 31 '18 17:07 gafter

Previous discussion: https://github.com/dotnet/csharplang/issues/535

alrz avatar Jul 31 '18 17:07 alrz

Similar issue which could be embraced by this proposal #1758

4creators avatar Aug 02 '18 18:08 4creators

This would not work with the current CLR spec, as stackalloc'ed memory cannot contain references.

gafter avatar Aug 03 '18 00:08 gafter

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).

alrz avatar Aug 03 '18 12:08 alrz

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).

alrz avatar Aug 07 '18 17:08 alrz

... I propose to disallow such overloads ...

What kind of rule did you have in mind for that?

gafter avatar Aug 07 '18 21:08 gafter

... 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 avatar Aug 07 '18 22:08 scalablecory

@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 A matches the total number of parameters. If A has 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 avatar Aug 07 '18 22:08 gafter

@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.

alrz avatar Aug 08 '18 05:08 alrz

@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.

alrz avatar Aug 08 '18 06:08 alrz

This would be another great case for #1447 Proposal: Customisable overload resolution

popcatalin81 avatar Aug 08 '18 12:08 popcatalin81

Would the upcoming work on object stack allocation in CoreCLR (see https://github.com/dotnet/coreclr/pull/20251) make this feature less important?

svick avatar Oct 04 '18 11:10 svick

@svick wow didn't know that's a thing. I think that would permit using stackalloced Span storage also with references?

alrz avatar Oct 04 '18 11:10 alrz

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 avatar Oct 04 '18 17:10 alrz

@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.

svick avatar Oct 04 '18 17:10 svick

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.

popcatalin81 avatar Oct 04 '18 18:10 popcatalin81

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 avatar Oct 04 '18 18:10 alrz

@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.

DarthVella avatar Oct 04 '18 23:10 DarthVella

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.

KillyMXI avatar Apr 04 '20 21:04 KillyMXI

Can't it just be: SomeType Foo(params<int> values); And C# treats it as: SomeType Foo(params Span<int> values);?

VBAndCs avatar Apr 12 '20 05:04 VBAndCs

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.

acaly avatar Nov 11 '20 00:11 acaly

This is great!

Cannot wait to see something like

public T Add<T>(params Span<T> values) where T : IMonoid<T>

Ultrahead avatar Nov 25 '20 22:11 Ultrahead

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 avatar Jul 29 '21 12:07 MichalPetryka

@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;
    }
}

ufcpp avatar Jul 30 '21 05:07 ufcpp

@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;
    }
}

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.

MichalPetryka avatar Jul 30 '21 12:07 MichalPetryka

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*/ }

MichalPetryka avatar Jul 30 '21 12:07 MichalPetryka

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.

Joe4evr avatar Aug 03 '21 09:08 Joe4evr

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.

KalleOlaviNiemitalo avatar Aug 03 '21 10:08 KalleOlaviNiemitalo

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.

jaredpar avatar Aug 03 '21 11:08 jaredpar

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.

Joe4evr avatar Aug 03 '21 13:08 Joe4evr