Why are there memory allocations when calling a func
Asked Answered
G

1

54

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?

enter image description here

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);
        }
    }
}
Gatekeeper answered 15/3, 2018 at 12:25 Comment(2)
that's because it is in debug mode, compiler will optimize the code when build is in release modePerutz
@EhsanSajjad: Nope, I don't believe it's because of that at all.Sharla
S
64

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.

Sharla answered 15/3, 2018 at 12:29 Comment(12)
I may be misremembering, but from ancient memory, I seem to recall that one of the blockers here is that when folks tried it, a number of cases were found where code depended (probably incorrectly) on the old behaviour, and broke without it. So since CLR/BCL changes are awkward to deploy, it wouldn't amaze me if the end state is "from v{something}, Roslyn will do this iff you're targeting NET{vSomething+} or NETSTANDARD{vSomethingElse+} or NETCOREAPP{vAnotherSomething+}". I could also be completely misremembering...Teacup
@MarcGravell: Given that it's in the spec, I guess it's not entirely unreasonable to depend on that, although it would feel like a code smell anyway to depend on reference inequality for the result of a method group conversion...Sharla
@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)" :)Teacup
@JonSkeet - Please could you point us to the part of the specification which requires a new delegate instance every time?Oeflein
@buffjape: Edited accordingly. (And it turns out the delegate creation expression version was similarly guaranteed to create a new instance, so I've removed that.)Sharla
But they are all static functions - partially evaluating them is easy. Making a new function out of calls to static functions should create a new static function. What's the problem?Oeflein
@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.Sharla
Thought the point of optimisation was to speed things up, keeping the same result. This code does nothing - fairly easy for the compiler to calculate that. surely?Oeflein
@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.Sharla
@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.Donaldson
@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.Sharla
I think there's an open issue to fix this here: github.com/dotnet/roslyn/issues/5835Slippy

© 2022 - 2024 — McMap. All rights reserved.