Chuu
Chuu

Reputation: 4509

What overhead do you pay when passing a function to a Func<> implicitly in a method call?

Let's say I have the following two methods:

    public void ConsumesDeletage (Func<object> consumer) { ... }
    public object DoesSomething() { ... }

In the same class, I have the following third method:

    public void ForceDelegateConsumption()
    {
        ConsumesDeletage(DoesSomething);
    }

Every time I call ForceDelegateConsumption(), what happens internally? Am I creating a new Func object for every single call? After the JITer is done with this code; does it turn into something equivalent to a simple function pointer and only has to be calculated once? Something else entirely?

Upvotes: 1

Views: 437

Answers (2)

Servy
Servy

Reputation: 203821

Reconstructing it from scratch is not particularly expensive, really, you're just creating a new object that has two fields, an object reference and a MethodInfo instance (or some equivalent).

It's almost certainly not worth trying to cache such delegates yourself, simply because it's such a fast operation to begin with.

Upvotes: 1

StriplingWarrior
StriplingWarrior

Reputation: 156469

Every time I call ForceDelegateConsumption(), what happens internally? Am I creating a new Func object for every single call?

Yes, but this tends to be a pretty inexpensive operation: you're just converting the "Method Group" into a Func<>. It's likely as fast or faster than whatever alternative you were considering.

Here's a benchmark to demonstrate. The times are so fast that I'm including a no-op so that we can capture how much of the time is spend in the benchmarking overhead:

void Main()
{
    // Enter setup code here
    Func<object> cachedDoesSomething = DoesSomething;
    var actions = new[]
    {
        new TimedAction("No-op", () => 
        {
        }),
        new TimedAction("method call", () => 
        {
            DoesSomething();
        }),
        new TimedAction("ForceDelegateConsumption", () => 
        {
            ForceDelegateConsumption();
        }),
        new TimedAction("ForceDelegateConsumptionInline", () => 
        {
            ConsumesDelegate(DoesSomething);
        }),
        new TimedAction("DoesSomethingInlined", () => 
        {
            ConsumesDelegate(() => null);
        }),
        new TimedAction("CachedLambda", () => 
        {
            ConsumesDelegate(cachedDoesSomething);
        }),
        new TimedAction("Explicit lambda", () => 
        {
            ConsumesDelegate(() => DoesSomething());
        }),   

    };
    const int TimesToRun = 10000000; // Tweak this as necessary
    TimeActions(TimesToRun, actions);
}
public void ConsumesDelegate (Func<object> consumer) { 
    consumer();
}
public object DoesSomething() { return null; }
public void ForceDelegateConsumption()
{
    ConsumesDelegate(DoesSomething);
}

Results:

Message                        DryRun1 DryRun2 FullRun1 FullRun2 
No-op                          0.046   0.0004   56.5705  57.3681 
method call                    0.1294  0.0004   96.169   98.9377 
ForceDelegateConsumption       0.2555  0.0004  315.6183 284.0828 
ForceDelegateConsumptionInline 0.1997  0.0012  278.4389 263.8278 
DoesSomethingInlined           0.2909  0.0008  145.8749 152.2732 
CachedLambda                   0.1388  0.0004  125.7794 135.8126 
Explicit lambda                0.2444  0.0004  308.683  304.2111 

All of the "FullRun" numbers are the milliseconds that it takes to run the method ten million times.

So you can see that my benchmark framework takes 50 ms even if we do nothing at all. Simply calling the DoSomething method, assuming it does nothing at all, takes another 50 ms to run ten million times. It then takes an additional 200 ms or less to call ConsumesDelegate, passing DoSomething to it as a method group (also ten million times).

If you're writing code that will get invoked many millions of times in a performance-critical path, you'll want to consider avoiding lambdas entirely. They add a tiny bit of memory overhead, and a tiny bit of extra CPU time. Otherwise, I wouldn't bother avoiding method-group conversion.

Upvotes: 4

Related Questions