Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Local Functions in C# - to capture or not to capture when passing parameters down?

When using Local Functions in C# 7 you have two options when you want to pass parameters (or other local variables) from the main method down to the local function: You can either explicitly declare the parameters as you would any other function or you can simply "capture" the parameters/variables from the containing method and use those directly.

An example perhaps illustrates this best:

Explicitly Declaring

public int MultiplyFoo(int id) {     return LocalBar(id);      int LocalBar(int number)     {         return number * 2;     } } 

Capturing

public int MultiplyFoo(int id) {     return LocalBar();      int LocalBar()     {         return id * 2;     } } 

Both methods work the same, but the way they invoke the local function is different.

So my question is:

Are there any difference between the two that I should be aware of? I'm thinking in terms of performance, memory allocation, garbage collection, maintainability etc.

like image 538
Dan Diplo Avatar asked Apr 03 '18 14:04

Dan Diplo


People also ask

What is a local function in C?

Variables that are declared inside a function or block are called local variables. They can be used only by statements that are inside that function or block of code. Local variables are not known to functions outside their own. The following example shows how local variables are used.

What is a local function?

Local functions are private methods of a type that are nested in another member. They can only be called from their containing member. Local functions can be declared in and called from: Methods, especially iterator methods and async methods.

What is global function and local function?

Local variable is declared inside a function whereas Global variable is declared outside the function. Local variables are created when the function has started execution and is lost when the function terminates, on the other hand, Global variable is created as execution starts and is lost when the program ends.


2 Answers

Local functions in C# are clever in terms of their capturing - at least in the Roslyn implementation. When the compiler is able to guarantee that you aren't creating a delegate from the local function (or doing something else that will prolong the lifetime of the variable) it can use a ref parameter with all the captured variables in a generated struct to communicate with the local function. For example, your second method would end up as something like:

public int MultiplyFoo(int id) {     __MultiplyFoo__Variables variables = new __MultiplyFoo__Variables();     variables.id = id;     return __Generated__LocalBar(ref variables); }  private struct __MultiplyFoo__Variables {     public int id; }  private int __Generated__LocalBar(ref __MultiplyFoo__Variables variables) {     return variables.id * 2; } 

So there's no heap allocation required as there would be for (say) a lambda expression converted to a delegate. On the other hand, there is the construction of the struct and then copying the values into that. Whether passing an int by value is more or less efficient than passing the struct by reference is unlikely to be significant... although I guess in cases where you had a huge struct as a local variable, it would mean that using implicit capture would be more efficient than using a simple value parameter. (Likewise if your local function used lots of captured local variables.)

The situation already gets more complicated when you have multiple local variables being captured by different local functions - and even more so when some of those are local functions within loops etc. Exploring with ildasm or Reflector etc can be quite entertaining.

As soon as you start doing anything complicated, like writing async methods, iterator blocks, lambda expressions within the local functions, using method group conversions to create a delegate from the local function etc... at that point I would hesitate to continue guessing. You could either try to benchmark the code each way, or look at the IL, or just write whichever code is simpler and rely on your bigger performance validation tests (which you already have, right? :) to let you know if it's a problem.

like image 194
Jon Skeet Avatar answered Oct 05 '22 23:10

Jon Skeet


It was an interesting question. First I've decompiled the build output.

public int MultiplyFoo(int id) {   return LocalFunctionTests.\u003CMultiplyFoo\u003Eg__LocalBar\u007C0_0(id); }  public int MultiplyBar(int id) {   LocalFunctionTests.\u003C\u003Ec__DisplayClass1_0 cDisplayClass10;   cDisplayClass10.id = id;   return LocalFunctionTests.\u003CMultiplyBar\u003Eg__LocalBar\u007C1_0(ref cDisplayClass10); } 

When you pass id as parameter, the local function get called with the passed id parameter. Nothing fancy and the parameter is stored on the stack frame of the method. However, if you don't pass over the parameter, a struct (thought named 'class' as Daisy pointed it out) gets created with a field (cDisplayClass10.id = id) and the id is assigned to it. Then the struct is passed as reference into the local function. C# compiler seems to do it to support closure.

In performance-wise, I used Stopwatch.ElapsedTicks, passing id as parameter was consistently faster. I think it's because of the cost of creating a struct with a field. The performance gap widened when I added another parameter to the local function.

  • Passing Id: 2247
  • Not passing Id: 2566

This is my tests code, if anyone's interested

public int MultiplyFoo(int id, int id2) {     return LocalBar(id, id2);      int LocalBar(int number, int number2)     {         return number * number2 * 2;     } }  public int MultiplyBar(int id, int id2) {     return LocalBar();      int LocalBar()     {         return id * id2 * 2;     } }   [Fact] public void By_Passing_Id() {     var sut = new LocalFunctions();      var watch = Stopwatch.StartNew();     for (int i = 0; i < 10000; i++)     {         sut.MultiplyFoo(i, i);     }      _output.WriteLine($"Elapsed: {watch.ElapsedTicks}"); }  [Fact] public void By_Not_Passing_Id() {     var sut = new LocalFunctions();      var watch = Stopwatch.StartNew();     for (int i = 0; i < 10000; i++)     {         sut.MultiplyBar(i, i);     }      _output.WriteLine($"Elapsed: {watch.ElapsedTicks}"); } 
like image 21
Andrew Chaa Avatar answered Oct 06 '22 00:10

Andrew Chaa