16

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

3 回答 3

13

常数 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 优化级别,您需要在发布模式下编译并在不附加调试器的情况下启动。即使在发布模式下,调试器也会阻止执行优化。

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

于 2012-06-14T21:47:25.223 回答
5

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

托尼丁字裤

于 2014-07-05T16:32:01.463 回答
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 表达式中消失,因为内联通常只保留给最简单的表达式。

于 2012-06-14T22:05:47.053 回答