31 July 2017

How are C# Local Functions Implemented?

One of the new features in C# 7 is local functions. They provide a more intuitive syntax over creating verbose System.Func delegates, as well as being more capable (they support ref and out parameters, async, generics, etc). I also read that local functions compile down to normal methods, thus reducing GC allocations when compared to System.Func.

I was curious about that last part. How does it work? Let’s open up the dotPeek decompiler and find out!

Decompiling a Local Function


First, here’s a simple test program using a local function:

Admittedly, the above local function is not really needed in this case, but it’s simple enough code that the decompilation won’t be scary!

After decompiling the above program, we get the following for the AddFive method:

The above comments are helpfully added by the decompiler. As we can see, the C# compiler created the following for us:
  • Program.<AddFive>g__InnerAdd1_0 – this is our InnerAdd function, converted to a normal static function in the Program class. <AddFive> is simply part of the name, it’s not a generic type. Note that if the enclosing method is an instance method, the generated function will be an instance method.
  • Program.<>c__DisplayClass1_0 – This is a generated class. It captures the a parameter, and is passed by reference into our function.
In order to look into the generated class and function, we need to inspect the IL code. Here is the IL code for the generated class that captures the a parameter:

Two interesting things about this are that it only has one field, int32 a, that is used to pass our a parameter to the function, and that the class extends from System.ValueType. System.ValueType is the base class for all value types, so the generated value type will not cause heap allocations. The C# compiler prevents user code from extending System.ValueType.

Next, let’s look at the generated method:

Despite being a bit long, this is pretty straight-forward. It’s a static function that takes two parameters, int b and our generated obj1. It loads our argument obj1 onto the stack, then loads field obj1.a, then loads our argument b. Next, it calls add, which pops the top two values off the stack and adds them, then pushes the result back on the stack. Finally, it calls ret to return that result.

Decompiling a Local Function, with mutation!


Let’s make things a bit more interesting. What if our nested function mutates (gasp)?

The InnerAdd function is now a void function, that mutates a in the outer scope. In this case, our decompiled AddFive function looks like this:

This is more interesting than the first case. We can see that our generated class is set up ahead of time, then passed into the generated static function, and then all subsequent references to the parameter a are rewritten into references to the generated field! Fascinating!

Conclusion


Hopefully, this post has shown that there's no magic going on when we use C#7's local functions. With this knowledge, we can begin to reason about the runtime and performance characteristics of local functions, and understand the benefits of local functions over System.Func declarations.