Alex Rivera | Logout

Performance of Func<T> and inheritance

Asked 2013-03-27T20:59:32.513
14

I've been having trouble with understanding the performance characteristics of using Func<...> throughout my code when using inheritance and generics - which is a combination I find myself using all the time.

Let me start with a minimal test case so we all know what we're talking about, then I'll post the results and then I'm going to explain what I would expect and why...

Minimal test case

public class GenericsTest2 : GenericsTest<int> 
{
    static void Main(string[] args)
    {
        GenericsTest2 at = new GenericsTest2();

        at.test(at.func);
        at.test(at.Check);
        at.test(at.func2);
        at.test(at.Check2);
        at.test((a) => a.Equals(default(int)));
        Console.ReadLine();
    }

    public GenericsTest2()
    {
        func = func2 = (a) => Check(a);
    }

    protected Func<int, bool> func2;

    public bool Check2(int value)
    {
        return value.Equals(default(int));
    }

    public void test(Func<int, bool> func)
    {
        using (Stopwatch sw = new Stopwatch((ts) => { Console.WriteLine("Took {0:0.00}s", ts.TotalSeconds); }))
        {
            for (int i = 0; i < 100000000; ++i)
            {
                func(i);
            }
        }
    }
}

public class GenericsTest<T>
{
    public bool Check(T value)
    {
        return value.Equals(default(T));
    }

    protected Func<T, bool> func;
}

public class Stopwatch : IDisposable
{
    public Stopwatch(Action<TimeSpan> act)
    {
        this.act = act;
        this.start = DateTime.UtcNow;
    }

    private Action<TimeSpan> act;
    private DateTime start;

    public void Dispose()
    {
        act(DateTime.UtcNow.Subtract(start));
    }
}

The results

Took 2.50s  -> at.test(at.func);
Took 1.97s  -> at.test(at.Check);
Took 2.48s  -> at.test(at.func2);
Took 0.72s  ->
Edit
Report

1 Answer

3

I'm going to explain what I think is going on here and with all generics. I needed some space to write, so I'm posting this as an answer. Thank you all for commenting and helping figuring this out, I'll make sure to award points here and there.

To get started...

Compiling generics

As we all know, generics are 'template' types where the compiler fills in the type information at run-time. It can make assumptions based on the constraints, but it doesn't change the IL code... (but more about that later).

A method from my question:

public class Foo<T>
{
    public void bool Handle(T foo) 
    {
        return foo.Equals(default(T));
    }
}

The constraints here are that T is an Object, which means the call to Equals is going to Object.Equals. Since T is implementing Object.Equals, this will look like:

L_0016: callvirt instance bool [mscorlib]System.Object::Equals(object)

We can improve on this by making it explicit that T implements Equals by adding the constraint T : IEquatable<T> . This changes the call to:

L_0011: callvirt instance bool [mscorlib]System.IEquatable`1<!T>::Equals(!0)

However, since T hasn't been filled in yet, apparently the IL doesn't support calling T::Equals(!0) directly even though it is surely there. The compiler can apparently only assume the constraint has been fulfilled, hence it needs to issue a call to IEquatable1` that defines the method.

Apparently hints like sealed don't make a difference, even though they should have.

Conclusion: Because T::Equals(!0) is not supported, a vtable lookup is required to make it work. Once it has become a callvirt, it's damn difficult for the JIT compiler to figure out that it should h

answered 2013-03-28T12:55:29.213

Your Answer