Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Don't emit unnecessary closures #11898

Open
jl0pd opened this issue Jul 27, 2021 · 6 comments
Open

Don't emit unnecessary closures #11898

jl0pd opened this issue Jul 27, 2021 · 6 comments

Comments

@jl0pd
Copy link

jl0pd commented Jul 27, 2021

Compiler generates method closure when that's unneeded.

Repro steps

Compile and run code

let handler (x: string) (y: int) =
    System.Console.WriteLine(x, y)

let invokeHandler (foo: System.Action<string, int>) =
    for parameter in foo.Method.GetParameters() do
        printf "%s " parameter.Name

let problematicMethod () =
    invokeHandler (System.Action<_,_>(handler))

[<EntryPoint>]
let main argv =
    problematicMethod()
    0 

Expected behavior

x y is written to console

Actual behavior

delegateArg0 delegateArg1 is written.

Compiler wraps method with static class and calls Invoke method from it. Looks like someone has optimised this scenario, but it would be better if compiler wont generate static class at all. Current behavior increases assembly size, causes unnecessary call and most important - mangles names, which causes bugs

internal static class problematicMethod@9
{
    internal static void Invoke(string delegateArg0, int delegateArg1)
    {
        Console.WriteLine(delegateArg0, delegateArg1); // small methods gets inlined
        // big methods aren't inlined
        // handler.Invoke(delegateArg0, delegateArg1);
    }
}

Known workarounds

Use anonymous function with explicit argument names

let fixedMethod () =
    invokeHandler (System.Action<_,_>(fun x y -> handler x y))

Related information

Windows 10, net5 .NET SDK (reflecting any global.json): Version: 5.0.302 Commit: c005824e35

Runtime Environment:
OS Name: Windows
OS Version: 10.0.19043
OS Platform: Windows
RID: win10-x64
Base Path: C:\Program Files\dotnet\sdk\5.0.302\

Host (useful for support):
Version: 5.0.8
Commit: 35964c9215

.NET SDKs installed:
3.1.411 [C:\Program Files\dotnet\sdk]
5.0.301 [C:\Program Files\dotnet\sdk]
5.0.302 [C:\Program Files\dotnet\sdk]

.NET runtimes installed:
Microsoft.AspNetCore.App 3.1.16 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
Microsoft.AspNetCore.App 3.1.17 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
Microsoft.AspNetCore.App 5.0.7 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
Microsoft.AspNetCore.App 5.0.8 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
Microsoft.NETCore.App 3.1.16 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
Microsoft.NETCore.App 3.1.17 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
Microsoft.NETCore.App 5.0.7 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
Microsoft.NETCore.App 5.0.8 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
Microsoft.WindowsDesktop.App 3.1.16 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]
Microsoft.WindowsDesktop.App 3.1.17 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]
Microsoft.WindowsDesktop.App 5.0.7 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]
Microsoft.WindowsDesktop.App 5.0.8 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]

@dsyme
Copy link
Contributor

dsyme commented Jul 27, 2021

I agree we should fix this

@dsyme dsyme added Bug Impact-Medium (Internal MS Team use only) Describes an issue with moderate impact on existing code. labels Jul 27, 2021
@kerams
Copy link
Contributor

kerams commented Jul 31, 2021

it would be better if compiler wont generate static class at all

But you need the closure to create an Action instance, don't you?

@jl0pd
Copy link
Author

jl0pd commented Aug 1, 2021

@kerams. No, closure isn't required, not for static methods nor for instance methods. They're only needed when you're creating anonymous function with captured parameters.

Delegates are just classes that inherit from System.MulticastDelegate. Each delegate have public constructor with object and IntPtr args. For example constructor of Action<T1, T2> have signature public extern Action(object @object, IntPtr method). If method is static - @object is null, otherwise @object should be instance of some class. IntPtr contains pointer to method.

Creating delegate requires several instructions:

ldnull // load null because method is static
ldftn void className::methodName(argType1, argType2)
newobj instance void class System.Action`2<argType1, argType2>::.ctor(object, native int)

Generating closure in this case is unnecessary. Instead of loading Program::problematicMethod(string, int) directly, compiler generates new class and calls Program/problematicMethod@9::Invoke(string, int)

If it were impossible to create action without closure compiler would've enter infinite loop of creating closure for closure for closure...

@kerams
Copy link
Contributor

kerams commented Aug 1, 2021

Well, I've learned something new, thanks.

@dsyme
Copy link
Contributor

dsyme commented Oct 4, 2021

I'd like to make progress on this issue, it might need an RFC.

There are several separate proposals lurking here and I'd like to tease them apart. The basic situation is

System.Action<_,_>(expr)

The cases we care about are where the target is a "known" funciton or method:

System.Action<_,_>(handler) // non-eta-expanded known target 
System.Action<_,_>(fun a b -> handler a b) // eta-expanded known target 
System.Action<_,_>(fun a b -> handler (a,b)) // eta-expanded known target with different currying/tupling but same compiled representation
System.Action<_,_>(SomeType.StaticMethod) // same with a static method
System.Action<_,_>(fun a b -> SomeType.StaticMethod a b) // same with a static method
System.Action<_,_>(fun a b -> SomeType.StaticMethod(a,b)) // same with a static method
System.Action<_,_>(SomeType<...>.StaticMethod<...>) // same with a generic static method 
System.Action<_,_>(fun a b -> SomeType<...>.StaticMethod<...> a b) // same with a generic static method
System.Action<_,_>(fun a b -> SomeType<...>.StaticMethod<...>(a,b)) // same with a generic static method
System.Action<_,_>(someObject.InstanceMethod) // same with an instance method
System.Action<_,_>(fun a b -> someObject.InstanceMethod a b) // same with an instance method
System.Action<_,_>(fun a b -> someObject.InstanceMethod(a, b)) // same with an instance method
System.Action<_,_>(fun a b -> someObject.InstanceMethod<...> a b) // same with a generic instance method
System.Action<_,_>(fun a b -> someObject.InstanceMethod<...>(a, b)) // same with a generic instance method

Arguably we also care about partial applications:

System.Action<_,_>(handler arg1 arg2) // non-eta-expanded known target via partial application

The queestions are

  1. Do we create a direct delegate? (that is, a delegate without a closure and with delegate.TargetMethod known to be the MethodInfo for the target)
  2. If not, do we create a closure with specific argument names

We have to consider these questions for both Debug and Release code.

The current situation and proposal is:

  1. Direct delegate?

    • For non-eta-expanded - no - proposal is to change this to "yes" for both debug and release code
    • For eta-expanded - no - proposal is to change this to "yes" for release code. The user has made the lambdas explicit, however release code can be more efficient eliminating intermediate closures. For debug code we would still generate a closure with the user-given argument names.
  2. Closure argument names drawn from direct target?

    • For non-eta-expanded - currently no we don't. Instead you get a closure with funny argument names "delegateArg0" etc. Since the proposal is to always generate a direct delegate, this is no longer an issue
    • For eta-expanded - currently we use the argument names derived from the patterns in the source code. This would not change for debug code. In release code, the proposal is to generate a direct delegate so the closure would no longer exist.
  3. Are these changes visible in quotations?

    • The proposal is that quotations wouldn't change.

This is in theory a potential breaking change because user code doing inspection on delegate value .TargetMethod may detect the difference. However within the context of the language spec it's a reasonable change as the TargetMethod is currently a closure. For this reason it is reasonable to make this change via an RFC and /langversion

@dsyme
Copy link
Contributor

dsyme commented Oct 4, 2021

I've written a language suggestion for this here: fsharp/fslang-suggestions#1083

@dsyme dsyme added Feature Request Needs-RFC and removed Bug Impact-Medium (Internal MS Team use only) Describes an issue with moderate impact on existing code. Area-Compiler labels Apr 5, 2022
@vzarytovskii vzarytovskii added this to the Backlog milestone Oct 19, 2022
@vzarytovskii vzarytovskii changed the title Unnecessary closures Don't emit unnecessary closures Nov 13, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Status: New
Development

No branches or pull requests

5 participants