为啥 lambda 比 IL 注入动态方法快?

Posted

技术标签:

【中文标题】为啥 lambda 比 IL 注入动态方法快?【英文标题】:Why is lambda faster than IL injected dynamic method?为什么 lambda 比 IL 注入动态方法快? 【发布时间】:2012-06-13 22:03:54 【问题描述】:

我刚刚构建了动态方法 - 见下文(感谢 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("\n0...\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("\n0...\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);
    
 

【问题讨论】:

我不能说我知道,但根据您使用它的目的,您可能需要查看免费的 Fasterflect api:fasterflect.codeplex.com 我在这里对您的代码进行的非正式测试中没有看到这一点 - mul1(IL 版本)在这里始终更快(大约 2 倍)。 我在问题中添加了输出。 mul2 快两倍。请在 x64 版本中构建,然后从控制台运行。不是来自 Visual Studio 内部。 老实说,我认为 CLR 是内联 lambda,不同之处在于 IL 方法是一个方法调用,而 lambda 只是循环中的一些代码.....这只是我能做的合理解释想出......但我无法证明它,因为你不能添加属性来禁用 lambda 上的内联(至少我不知道它是如何完成的)......如果我能做到这一点,它可能表明我的理论是正确的.. CLR 不内联委托调用。 【参考方案1】:

原因是常数 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 不会以任何方式内联委托调用。之所以提到这一点,是因为人们错误地推测这就是 cmets 的情况。

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

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

【讨论】:

你是倒着读的。生成的方法比 lambda 表达式慢 2 倍。您的论点支持生成的方法比 lambda 表达式更快。 嗯,好吧,看来我倒退了。我为 x86 和 x64 重新制作了这个。我检查了 IL 代码,我所说的一切看起来都是真的。然而,数字不会说谎!嗯... Visual Studio 不允许我调试 EmbedConst func 的 JITed 代码。 @usr:现在你也让我困惑了! 我找到了原因并添加了我的解释。 @leppie,这看起来像是 lambda 主体的 JITed 代码。但是,您无法访问生成方法的 JITed 代码。【参考方案2】:

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

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

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

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

托尼丁字裤

【讨论】:

【参考方案3】:

鉴于只有在没有附加调试器的情况下以发布模式运行时才存在性能差异,我能想到的唯一解释是 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 处理。

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

【讨论】:

以上是关于为啥 lambda 比 IL 注入动态方法快?的主要内容,如果未能解决你的问题,请参考以下文章

SqlDataReader生成动态Lambda表达式

lambda 比 python 中的函数调用慢,为啥

动态IL织入框架Harmony简单入手

究竟是什么可以比反射还快实现动态调用?| Source Generators版

.NET Core中的一个接口多种实现的依赖注入与动态选择看这篇就够了

elasticsearch为啥快