为什么lambda比IL注入动态方法更快?

时间:2012-06-13 22:03:55

标签: c# .net-4.0

我刚刚建立了动态​​方法 - 见下文(感谢SO用户)。似乎Func创建为动态方法,IL注入比lambda慢2倍。

任何人都知道为什么?

(编辑:这是在VS2010中构建为Release x64。请从控制台运行,而不是从Visual Studio F5内部运行。)

class Program
{
    static void Main(string[] args)
    {
        var mul1 = IL_EmbedConst(5);
        var res = mul1(4);

        Console.WriteLine(res);

        var mul2 = EmbedConstFunc(5);
        res = mul2(4);

        Console.WriteLine(res);

        double d, acc = 0;

        Stopwatch sw = new Stopwatch();

        for (int k = 0; k < 10; k++)
        {
            long time1;

            sw.Restart();

            for (int i = 0; i < 10000000; i++)
            {
                d = mul2(i);
                acc += d;
            }

            sw.Stop();

            time1 = sw.ElapsedMilliseconds;

            sw.Restart();

            for (int i = 0; i < 10000000; i++)
            {
                d = mul1(i);
                acc += d;
            }

            sw.Stop();

            Console.WriteLine("{0,6} {1,6}", time1, sw.ElapsedMilliseconds);
        }

        Console.WriteLine("\n{0}...\n", acc);
        Console.ReadLine();
    }

    static Func<int, int> IL_EmbedConst(int b)
    {
        var method = new DynamicMethod("EmbedConst", typeof(int), new[] { typeof(int) } );

        var il = method.GetILGenerator();

        il.Emit(OpCodes.Ldarg_0);
        il.Emit(OpCodes.Ldc_I4, b);
        il.Emit(OpCodes.Mul);
        il.Emit(OpCodes.Ret);

        return (Func<int, int>)method.CreateDelegate(typeof(Func<int, int>));
    }

    static Func<int, int> EmbedConstFunc(int b)
    {
        return a => a * b;
    }
}

这是输出(对于i7 920)

20
20

25     51
25     51
24     51
24     51
24     51
25     51
25     51
25     51
24     51
24     51

4.9999995E+15...

=============================================== =============================

编辑编辑编辑

以下证明 dhtorpe 是正确的 - 更复杂的lambda将失去其优势。 用于证明它的代码(这表明Lambda与IL注入具有完全相同的性能):

class Program
{
    static void Main(string[] args)
    {
        var mul1 = IL_EmbedConst(5);
        double res = mul1(4,6);

        Console.WriteLine(res);

        var mul2 = EmbedConstFunc(5);
        res = mul2(4,6);

        Console.WriteLine(res);

        double d, acc = 0;

        Stopwatch sw = new Stopwatch();

        for (int k = 0; k < 10; k++)
        {
            long time1;

            sw.Restart();

            for (int i = 0; i < 10000000; i++)
            {
                d = mul2(i, i+1);
                acc += d;
            }

            sw.Stop();

            time1 = sw.ElapsedMilliseconds;

            sw.Restart();

            for (int i = 0; i < 10000000; i++)
            {
                d = mul1(i, i + 1);
                acc += d;
            }

            sw.Stop();

            Console.WriteLine("{0,6} {1,6}", time1, sw.ElapsedMilliseconds);
        }

        Console.WriteLine("\n{0}...\n", acc);
        Console.ReadLine();
    }

    static Func<int, int, double> IL_EmbedConst(int b)
    {
        var method = new DynamicMethod("EmbedConstIL", typeof(double), new[] { typeof(int), typeof(int) });

        var log = typeof(Math).GetMethod("Log", new Type[] { typeof(double) });

        var il = method.GetILGenerator();

        il.Emit(OpCodes.Ldarg_0);
        il.Emit(OpCodes.Ldc_I4, b);
        il.Emit(OpCodes.Mul);
        il.Emit(OpCodes.Conv_R8);

        il.Emit(OpCodes.Ldarg_1);
        il.Emit(OpCodes.Ldc_I4, b);
        il.Emit(OpCodes.Mul);
        il.Emit(OpCodes.Conv_R8);

        il.Emit(OpCodes.Call, log);

        il.Emit(OpCodes.Sub);

        il.Emit(OpCodes.Ret);

        return (Func<int, int, double>)method.CreateDelegate(typeof(Func<int, int, double>));
    }

    static Func<int, int, double> EmbedConstFunc(int b)
    {
        return (a, z) => a * b - Math.Log(z * b);
    }
} 

3 个答案:

答案 0 :(得分:12)

常数5是原因。为什么到底是这样?原因:当JIT知道常量为5时,它不会发出imul指令而是发出lea [rax, rax * 4]。这是众所周知的汇编级优化。但由于某种原因,此代码执行速度较慢。优化是一种悲观。

发出闭包的C#编译器阻止了JIT以特定方式优化代码。

证明:将常量更改为56878567并且性能会发生变化。检查JITed代码时,您可以看到现在使用了imul。

我设法通过将常量5硬编码到lambda中来捕获这个:

    static Func<int, int> EmbedConstFunc2(int b)
    {
        return a => a * 5;
    }

这让我可以检查JITed x86。

旁注:.NET JIT不以任何方式内联委托调用。只是提到这一点,因为错误地推测这是评论中的情况。

Sidenode 2:为了获得完整的JIT优化级别,您需要在Release模式下进行编译,并在没有附加调试器的情况下启动。即使在发布模式下,调试器也会阻止执行优化。

旁注3:虽然EmbedConstFunc包含一个闭包,并且通常比动态生成的方法慢,但这种“lea”优化的效果会造成更大的伤害并最终变慢。

答案 1 :(得分:5)

lambda并不比DynamicMethod快。它基于。但是,静态方法比实例方法快,但静态方法的委托创建比委托创建实例方法慢。 Lambda表达式构建一个静态方法,但通过添加“Closure”作为第一个paameter,就像实例方法一样使用它。委托静态方法“pop”堆栈去除“mov”到真正的“IL body”之前不需要的“this”实例。例如,在委托方法的情况下,“IL body”被直接命中。这就是为什么通过lambda表达式构建一个hypotetic静态方法的委托更快(可能是实例/静态方法之间委托模式代码共享的副作用)

可以通过向DynamicMethod添加未使用的第一个参数(例如Closure类型)并使用显式目标实例调用CreateDelegate(可以使用null)来避免性能问题。

var myDelegate = DynamicMethod.CreateDelegate(MyDelegateType,null)as MyDelegateType;

http://msdn.microsoft.com/fr-fr/library/z43fsh67(v=vs.110).aspx

Tony THONG

答案 2 :(得分:2)

鉴于只有在没有附加调试器的发布模式下运行时才存在性能差异,我能想到的唯一解释是JIT编译器能够对lambda表达式进行本机代码优化,而它无法执行用于发射的IL动态函数。

编译发布模式(优化)并在没有附加调试器的情况下运行,lambda始终比生成的IL动态方法快2倍。

使用附加到该进程的调试器运行相同的发布模式优化构建会使lambda性能降低到与生成的IL动态方法相当或更差。

这两次运行之间的唯一区别在于JIT的行为。在调试进程时,JIT编译器会抑制许多本机代码生成优化,以保留IL指令的本机指令,以源代码行号映射和其他可能会被激进的本机指令优化所破坏的相关性。

当输入表达式图形(在本例中为IL代码)与某些非常特定的模式和条件匹配时,编译器只能应用特殊情况优化。 JIT编译器清楚地具有lambda表达式IL代码模式的特殊知识,并且为lambdas发出不同于“普通”IL代码的代码。

很可能您的IL指令与导致JIT编译器优化lambda表达式的模式不完全匹配。例如,您的IL指令将B值编码为内联常量,而类似的lambda表达式从内部捕获的变量对象实例加载字段。即使你生成的IL是模仿C#编译器生成的lambda表达式IL的捕获字段模式,它仍然可能不够“足够接近”以接收与lambda表达式相同的JIT处理。

正如评论中所提到的,这可能是由于内联lambda以消除呼叫/返回开销。如果是这种情况,我希望看到这种性能上的差异在更复杂的lambda表达式中消失,因为内联通常只保留给最简单的表达式。