Why Local Functions generate IL different from Anonymous Methods and Lambda Expressions?
Asked Answered
H

2

15

Why the C# 7 Compiler turns Local Functions into methods within the same class where their parent function is. While for Anonymous Methods (and Lambda Expressions) the compiler generates a nested class for each parent function, that will contain all of its Anonymous Methods as instance methods ?

For example, C# code (Anonymous Method):

internal class AnonymousMethod_Example
{
    public void MyFunc(string[] args)
    {
        var x = 5;
        Action act = delegate ()
        {
            Console.WriteLine(x);
        };
        act();
    }
}

Will produce IL Code (Anonymous Method) similar to:

.class private auto ansi beforefieldinit AnonymousMethod_Example
{
    .class nested private auto ansi sealed beforefieldinit '<>c__DisplayClass0_0'
    {
        .field public int32 x

        .method assembly hidebysig instance void '<MyFunc>b__0' () cil managed 
        {
            ...
            AnonymousMethod_Example/'<>c__DisplayClass0_0'::x
            call void [mscorlib]System.Console::WriteLine(int32)
            ...
        }
        ...
    }
...

While this, C# code (Local Function):

internal class LocalFunction_Example
{
    public void MyFunc(string[] args)
    {
        var x = 5;
        void DoIt()
        {
            Console.WriteLine(x);
        };
        DoIt();
    }
}

Will generate IL Code (Local Function) similar to:

.class private auto ansi beforefieldinit LocalFunction_Example
{
    .class nested private auto ansi sealed beforefieldinit '<>c__DisplayClass0_0' extends [mscorlib]System.ValueType
    {
        .field public int32 x
    }

    .method public hidebysig instance void MyFunc(string[] args) cil managed 
    {
        ...
        ldc.i4.5
        stfld int32 LocalFunction_Example/'<>c__DisplayClass1_0'::x
        ...
        call void LocalFunction_Example::'<MyFunc>g__DoIt1_0'(valuetype LocalFunction_Example/'<>c__DisplayClass1_0'&)
    }

    .method assembly hidebysig static void '<MyFunc>g__DoIt0_0'(valuetype LocalFunction_Example/'<>c__DisplayClass0_0'& '') cil managed 
    {
        ...
        LocalFunction_Example/'<>c__DisplayClass0_0'::x
        call void [mscorlib]System.Console::WriteLine(int32)
         ...
    }
}

Note that DoIt function has turned into a static function in the same class as its parent function. Also the enclosed variable x has turned into a field in a nested struct (not nested class as in the Anonymous Method example).

Harijan answered 26/7, 2017 at 21:45 Comment(8)
Try implementing a lambda the way that local methods are implemented and see what happens.Potboiler
@Potboiler would you please tell me how to do that ?Harijan
You won't be able to. That's the point.Potboiler
@Potboiler this is exactly what I'm asking about. Why me (or the compiler guys) cannot implement the lambda the way that local methods are ?Harijan
So try implementing a lambda using the same method that the local method uses, and see why it doesn't work.Potboiler
@Potboiler Both anonymous and local methods can be called from any code. The CIL hasn't changed. But the purpose is different. Anonymous methods are made to be called from outside code, and local methods are made to be called from inside code, but that doesn't mean you cannot use them interchangeably. You can turn a local method into a delegate and pass it to any code you wish. You can make a delegate never leave the original method, and it will work like a local method. Doing so would just be pointless, but not impossible.Concerned
@IllidanS4 You're talking about using the C# code for each operation interchangeably, which you are indeed able to do. What you're not able to do is use the CIL representations of those things interchangeably, or to implement one with the other. If you use a local method as a delegate it will apply a transformation to it in order for it to act like a lambda. If you just used the CIL for a local method as if it were a lambda it wouldn't work.Potboiler
The accepted answer is still incorrect.Concerned
D
14

Anonymous methods stored in delegates may be called by any code, even code written in different languages, compiled years before C# 7 came out, and the CIL generated by the compiler needs to be valid for all possible uses. This means in your case, at the CIL level, the method must take no parameters.

Local methods can only be called by the same C# project (from the containing method, to be more specific), so the same compiler that compiles the method will also be handled to compile all calls to it. Such compatibility concerns as for anonymous methods therefore don't exist. Any CIL that produces the same effects will work here, so it makes sense to go for what's most efficient. In this case, the re-write by the compiler to enable the use of a value type instead of a reference type prevents unnecessary allocations.

Darter answered 26/7, 2017 at 21:55 Comment(10)
Even if the delegate could be re-written to a method that accepts a parameter, it wouldn't work. It's not about being compatible with other systems or earlier code, it's that the problem cannot be solved using that method in the case of delegates. The two bits of code do radically different things. Since they do radically different things, their implementation is, understandably, different.Potboiler
@Potboiler I'm not disagreeing, but I'm looking at it from a different point of view. The delegate could be re-written to a method that accepts a parameter of a different type, if the compiler could see and modify all invocations of the delegate. But of course there's no way the compiler can see and modify all invocations.Darter
And even if it could modify all invocations you still wouldn't be able to solve the problem because the values that the method needs won't exist at many of those call sites to pass in, so the problems are deeper and more fundamental than that.Potboiler
@Potboiler That part is possible even in the general case even with value types: it could be done with a static method bound to a boxed copy of the value type. The compiler could avoid the boxing when it was statically provable that the lifetime of the delegate did not exceed the method. But not worth the effort.Darter
Now you're saying that the closure type created could be a value type, and yes, it could be, although it wouldn't be productive to do so, as you mentioned. That approach is still different from what the local method version is doing.Potboiler
@Potboiler In what way? Both versions use a container type to hold the local variables. Both versions use a method that effectively takes a single argument to that container type. One happens to be the implicit this of an instance method, the other happens to be the first parameter of a static method. Other than that, what difference do you see that I'm missing?Darter
@hvd Thanks for fixing the string typo.. It made me puzzled for a while :)Harijan
@hvd and what do you mean by "Local methods can only be called by the same C# project" ? Is it possible to call a local function from outside its parent ?! .. If yes, would you please give me a link to an example.Harijan
@Harijan No, that's not possible, and wasn't what I was trying to say. Is it clearer like this?Darter
This answer is plainly wrong. Both anonymous methods and locally-defined methods can be passed to any code that accepts a delegate, no matter its original language. The implementation is different, but it makes no difference for the usage.Concerned
C
5

The primary usage of anonymous methods (and lambda expressions) is the ability to pass them to a consuming method to specify a filter, predicate or whatever the method wants. They were not specifically suited for being called from the same method that defined them, and that ability was considered only later on, with the System.Action delegate.

On the other hand, local methods are the precise opposite - their primary purpose is to be called from the same method, like using a local variable.

Anonymous methods can be called from within the original method, but they were implemented in C# 2, and this specific usage wasn't taken into consideration.

So local methods can be passed to other methods, but their implementation details were designed in such a way that would be better for their purpose. After all, the difference you are observing is a simple optimisation. They could have optimised anonymous methods this way back in the day, but they didn't, and adding such optimisation now could potentially break existing programs (although we all know that relying on an implementation detail is a bad idea).

So let's see where the optimisation lies. The most important change is the struct instead of class. An anonymous method needs a way to access the outside local variables even after the original method returns. This is called a closure, and the DisplayClass is what implements it. The main difference between C function pointers and C# delegates is that a delegate may optionally also carry a target object, simply used as this (the first argument internally). The method is bound to the target object, and the object is passed to the method every time the delegate is invoked (internally as the first argument, and the binding actually works even for static methods).

However, the target object is... an object. You can bind a method to a value type, but it needs to be boxed prior to this. Now you can see why the DisplayClass needs to be a reference type in case of an anonymous method, because a value type will be a burden, not an optimisation, requiring additional boxing.

Using a local method removes the need of binding a method to an object, and the consideration of passing the method to outside code. We can allocate the DisplayClass purely on the stack (as it should be for local data), or generally in the same place as the original local variables, presenting no burden on the GC. Now the developers had two choices ‒ either make the LocalFunc an instance method and move it to the DisplayClass, or make it static and make the DisplayClass its first (ref) parameter. There is no difference between those two options of calling the method, so I think the choice was simply arbitrary. They could've decided otherwise, without any difference.

However, notice how quickly this optimisation is dropped once it could turn into a performance issue. A simple addition to your code, like Action a = DoIt; would immediately change the LocalFunc method. The result immediately reverts to the case of the anonymous method, because the DisplayClass would need boxing etc.

Concerned answered 31/7, 2017 at 10:8 Comment(5)
Have you got a reference for this? "Anonymous methods can be called from within the original method, but they were implemented in C# 2, and this specific usage wasn't taken into consideration."Exhibit
@Exhibit I do not have any special insight into the development process at the time of C# 2, but it follows from the usage. At the time, anonymous methods were primarily designed to be used alongside events, i.e. as += delegate, seen from the choice of the delegate keyword and the possibility of ignoring any parameters. Using them in a different way required providing a delegate type, so even if the case of calling the delegate from the same method might have been considered, it did not have any effect on the way the code is generated. After all, you had to use a variable to store it.Concerned
I think you're reaching to say something was considered or not. The C# design team are very thorough.Exhibit
Seems that local functions can also be used as predicates no? Saying they are the opposite seems misleadingJumbo
@DouglasGaskell Do I? "So local methods can be passed to other methods", or are you referring to the word "local" in the sense that they are no longer local if they are exposed somewhere else?Concerned

© 2022 - 2024 — McMap. All rights reserved.