I have the following program which construct a local Func from two static methods. But strangely, when I profile the program, it allocated close to a million Func objects. Why invoking Func object is also creating Func instances?
public static class Utils
{
public static bool ComparerFunc(long thisTicks, long thatTicks)
{
return thisTicks < thatTicks;
}
public static int Foo(Guid[] guids, Func<long, long, bool> comparerFunc)
{
bool a = comparerFunc(1, 2);
return 0;
}
}
class Program
{
static void Main(string[] args)
{
Func<Guid[], int> func = x => Utils.Foo(x, Utils.ComparerFunc);
var guids = new Guid[10];
for (int i = 0; i < 1000000; i++)
{
int a = func(guids);
}
}
}
You're using a method group conversion to create the Func<long, long, bool>
used for the comparerFunc
parameter. Unfortunately, the C# 5 specification currently requires that to create a new delegate instance each time it's run. From the C# 5 specification section 6.6, describing the run-time evaluation of a method group conversion:
A new instance of the delegate type D is allocated. If there is not enough memory available to allocate the new instance, a System.OutOfMemoryException is thrown and no further steps are executed.
The section for anonymous function conversions (6.5.1) includes this:
Conversions of semantically identical anonymous functions with the same (possibly empty) set of captured outer variable instances to the same delegate types are permitted (but not required) to return the same delegate instance.
... but there's nothing similar for method group conversions.
That means this code is permitted to be optimized to use a single delegate instance for each of the delegates involved - and Roslyn does.
Func<Guid[], int> func = x => Utils.Foo(x, (a, b) => Utils.ComparerFunc(a, b));
Another option would be to allocate the Func<long, long, bool>
once and store it in a local variable. That local variable would need to be captured by the lambda expression, which prevents the Func<Guid[], int>
from being cached - meaning that if you executed Main
many times, you'd create two new delegates on each call, whereas the earlier solution would cache as far as is reasonable. The code is simpler though:
Func<long, long, bool> comparer = Utils.ComparerFunc;
Func<Guid[], int> func = x => Utils.Foo(x, comparer);
var guids = new Guid[10];
for (int i = 0; i < 1000000; i++)
{
int a = func(guids);
}
All of this makes me sad, and in the latest edition of the ECMA C# standard, the compiler will be permitted to cache the result of method group conversions. I don't know when/whether it will do so though.
@JonSkeet I agree entirely; and it will be a huge benefit if it happens - in code reviews I often see (and fix) cases where method groups are used in ways that allocate, where lambda-izing it (is that a word?) doesn't - but a: I'm not perfect, and b: lots of people don't know about this (and why should they have to?). So: if you want to club together we could send Jared a beer or two "totally not in a bribing way, honest (but please make it happen kthxbye)" :)
@buffjape: "What's the problem?" The compiler is obeying the specification, that's all. If the specification didn't say that a new delegate had to be created each time, it could easily cache the result as it does for the lambda expression.
@buffjape: If the optimization weren't detectable, that would be fine. In this case, it is detectable, because you can tell that multiple delegate instances are being created as per the spec. That part of the "result" isn't kept the same, so it's not a pure optimization.
@JonSkeet There is a level of whole-program optimization that would reveal that nothing actually depends on the identity of
comparerFunc
in this case. But I'm not surprised that doesn't happen.@TavianBarnes: That's definitely not the C# compiler's job, IMO. The JIT compiler could potentially do this - but it's not clear to me that this is a problem often enough for it to justify the JIT-time cost.