views:

59

answers:

2

I would have expected delegates generated from expression trees to achieve roughly the same performance as hard-coded, static, equivalent anonymous methods. However, it seems that dynamically generated delegates are noticeably slower...

Here's a simple test program to illustrate the case. It just accesses 3 properties of an object 1000000 times:

static void Main()
{
    var foo = new Foo { A = 42, B = "Hello world", C = new DateTime(1970, 1, 1) };

    Func<Foo, int> getA;
    Func<Foo, string> getB;
    Func<Foo, DateTime> getC;

    // Using hard-coded lambdas
    getA = f => f.A;
    getB = f => f.B;
    getC = f => f.C;
    Console.WriteLine("Hard-coded: {0}", Test(foo, getA, getB, getC));

    // Using dynamically generated delegates
    ParameterExpression prm = Expression.Parameter(typeof(Foo), "foo");
    getA = Expression.Lambda<Func<Foo, int>>(Expression.Property(prm, "A"), prm).Compile();
    getB = Expression.Lambda<Func<Foo, string>>(Expression.Property(prm, "B"), prm).Compile();
    getC = Expression.Lambda<Func<Foo, DateTime>>(Expression.Property(prm, "C"), prm).Compile();
    Console.WriteLine("Generated:  {0}", Test(foo, getA, getB, getC));
}

const int N = 1000000;

static TimeSpan Test(Foo foo, Func<Foo, int> getA, Func<Foo, string> getB, Func<Foo, DateTime> getC)
{
    var sw = Stopwatch.StartNew();
    for (int i = 0; i < N; i++)
    {
        getA(foo);
        getB(foo);
        getC(foo);
    }
    sw.Stop();
    return sw.Elapsed;
}

public class Foo
{
    public int A { get; set; }
    public string B { get; set; }
    public DateTime C { get; set; }
}

I consistently get results showing that hard-coded lambdas are about 6 times faster:

Hard-coded: 00:00:00.0115959
Generated:  00:00:00.0735896

Hard-coded: 00:00:00.0113993
Generated:  00:00:00.0648543

Hard-coded: 00:00:00.0115280
Generated:  00:00:00.0611804

Could anyone explain these results ? Is that due to compiler optimizations ? or JIT optimizations ?

Thanks for your insight


EDIT: I was running my tests with LINQPad, which compiles with optimizations enabled. When I run the same tests in VS with optimizations disabled, I get roughly the same results in both cases. So it seems the compiler was just inlining the property access in the hard-coded lambdas...

Bonus question: is there a way to optimize code generated from expression trees ?

+3  A: 

Just a guess, but i think it's the optimizer that sees that the Lambda expressions are simple getters and turns them into direct access or similar. The generated Expressions cannot be optimized so they result in slower code.

It should happen at compile time so you should try it with optimizations disabled and check the results again.

dbemerlin
It seems you're right... I ran my tests in LINQPad, which compiles with optimizations enabled. I just tried again with VS, and when I compile without optimizations, I get the same results for both types of delegate.
Thomas Levesque
+2  A: 

There's something else going on. The benchmark is critically dependent on the .NET Platform Target you select. I do repro the results for .NET 4.0 but .NET 3.5 is consistently faster on the generated version, regardless of the build type.

Guessing what might have changed in 4.0 to make it so much slower is too difficult, this is not easy code to analyze. And expression tree support has significantly changed. I recommend you post feedback to connect.microsoft.com, do expect a "by design" response. Hopefully annotated.

Hans Passant
Interesting... I only tried it with 4.0. It's quite surprising that the generated version is faster in 3.5 ! Anyway, I don't intend to post on Connect, as I don't consider it a bug. I was merely wondering what was causing the difference.
Thomas Levesque
Yes, that it was *faster* surprised me too. It's impressive.
Hans Passant