Skip to content

Allow unsealed delegates to reduce allocations for lambdas #8362

Closed
@svick

Description

@svick

Currently, the requirements for delegates are (from ECMA-335 §II.14.6 Delegates):

Delegates shall be declared sealed, and the only members a delegate shall have are [the] methods as specified here.

What if this requirement was weakened, so that delegates were allowed to be unsealed and also allowed to contain other members, especially fields? I think it would allow the C# compiler (and other compilers) to reduce the number of allocations required for lambdas that close over local variables, by fusing the delegate and the closure into a single object. (It also might allow F# to start using delegate types instead of FSharpFunc, but I'm not sure about that.)

For example, consider the following C# code:

delegate int FuncInt();

void M()
{
    int i = 42;
    FuncInt f = () => i;
    f();
}

Currently, the generated code looks roughly like this:

sealed class FuncInt : MulticastDelegate
{
    public extern FuncInt(object obj, IntPtr method);

    public virtual extern int Invoke();
}

sealed class Closure
{
    public int i;

    internal int Main_f() => this.i;
}

void Main()
{
    Closure closure = new Closure();
    closure.i = 42;
    FuncInt f = new FuncInt(closure,  __methodptr(Closure.Main_f));
    f.Invoke();
}

Notice that there are two allocations for a single lambda: the closure and the delegate.

If the requirements for delegates were weakened, the generated code could instead be:

class FuncInt : MulticastDelegate // no longer sealed
{
    public extern FuncInt(object obj, IntPtr method);

    public virtual extern int Invoke();
}

sealed class Closure : FuncInt // delegate class containing fields and custom methods
{
    public Closure() : base(this, __methodptr(Main_f)) {}

    public int i;

    private int Main_f() => this.i;
}

void Main()
{
    Closure closure = new Closure();
    closure.i = 42;
    FuncInt f = closure;
    f.Invoke();
}

This way, the two allocations are fused, which should make the code more efficient.

Some other notes:

  • In cases when a single closure is used for multiple lambdas, this would still avoid one delegate allocation (but not the others).
  • I think this also shouldn't interfere with the multicasting nature of delegates, since the runtime-provided fields would still be there.
  • Since I believe that unsealing a class is not a breaking change, unsealing framework-provided delegate types (like the Func and Action types) in some future version of the framework should be fine. Existing code would then become more efficient just by using a newer compiler and targeting a newer framework.

What do you think?

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions