简体   繁体   中英

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.

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.

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}");
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM