Reputation: 4509
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
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
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