.NET动态方法。最佳性能

4
什么是创建动态方法的最佳方式,但其效率与在VS中编译的方法相同?
比如我想创建一个计算器。用户输入公式,例如A+B/C*0.5;
我想要创建类似于Func的东西,它将接受A、B、C作为double类型参数,并返回double类型。
参数类型和返回类型始终为double。参数数量可变但至少为1个。
这些公式可能经常更改/添加。一旦“编译”了一个公式,它将成为低延迟代码的一部分,可以被调用1000次/秒。
我需要找到一种简单可靠的方法来构建它,但它必须具有静态构建和优化方法的精确性能特性

你已经有获取运算符的机制了吗? - Chris Gessler
不,我只是在规划中... - Boppity Bop
4个回答

12

我在这里找到了Microsoft关于此的博客(使用Expression Trees生成动态方法),并且比较了静态方法、编译后的表达式树和IL注入之间的性能。

以下是代码:

    static void Main(string[] args)
    {
        double acc = 0;

        var il = ILFact();
        il.Invoke(1);

        var et = ETFact();
        et(1);

        Stopwatch sw = new Stopwatch();

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

            sw.Restart();

            for (int i = 0; i < 30000; i++)
            {
                var result = CSharpFact(i);
                acc += result;
            }

            sw.Stop();

            time1 = sw.ElapsedMilliseconds;

            sw.Restart();

            for (int i = 0; i < 30000; i++)
            {
                double result = il.Invoke(i);
                acc += result;
            }

            sw.Stop();

            time2 = sw.ElapsedMilliseconds;

            sw.Restart();

            for (int i = 0; i < 30000; i++)
            {
                var result = et(i);
                acc += result;
            }

            sw.Stop();

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

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

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

        var il = method.GetILGenerator();
        var result = il.DeclareLocal(typeof(int));
        var startWhile = il.DefineLabel();
        var returnResult = il.DefineLabel();

        // result = 1

        il.Emit(OpCodes.Ldc_I4_1);
        il.Emit(OpCodes.Stloc, result);

        // if (value <= 1) branch end

        il.MarkLabel(startWhile);
        il.Emit(OpCodes.Ldarg_0);
        il.Emit(OpCodes.Ldc_I4_1);
        il.Emit(OpCodes.Ble_S, returnResult);

        // result *= (value--)

        il.Emit(OpCodes.Ldloc, result);
        il.Emit(OpCodes.Ldarg_0);
        il.Emit(OpCodes.Dup);
        il.Emit(OpCodes.Ldc_I4_1);
        il.Emit(OpCodes.Sub);
        il.Emit(OpCodes.Starg_S, 0);
        il.Emit(OpCodes.Mul);
        il.Emit(OpCodes.Stloc, result);

        // end while

        il.Emit(OpCodes.Br_S, startWhile);

        // return result

        il.MarkLabel(returnResult);
        il.Emit(OpCodes.Ldloc, result);
        il.Emit(OpCodes.Ret);

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

    static Func<int, int> ETFact()
    {
        // Creating a parameter expression.
        ParameterExpression value = Expression.Parameter(typeof(int), "value");

        // Creating an expression to hold a local variable. 
        ParameterExpression result = Expression.Parameter(typeof(int), "result");

        // Creating a label to jump to from a loop.
        LabelTarget label = Expression.Label(typeof(int));

        // Creating a method body.
        BlockExpression block = Expression.Block(

        // Adding a local variable.
        new[] { result },

        // Assigning a constant to a local variable: result = 1
        Expression.Assign(result, Expression.Constant(1)),

        // Adding a loop.
        Expression.Loop(

        // Adding a conditional block into the loop.
        Expression.IfThenElse(

        // Condition: value > 1
        Expression.GreaterThan(value, Expression.Constant(1)),

        // If true: result *= value --
        Expression.MultiplyAssign(result,
        Expression.PostDecrementAssign(value)),

        // If false, exit from loop and go to a label.
        Expression.Break(label, result)
        ),

        // Label to jump to.
        label
        )
        );

        // Compile an expression tree and return a delegate.
        return Expression.Lambda<Func<int, int>>(block, value).Compile();
    }

    static int CSharpFact(int value)
    {
        int result = 1;
        while (value > 1)
        {
            result *= value--;
        }

        return result;
    }

以下是在i7-920上进行的3次运行。构建版本:Release x64

583    542    660
577    578    666
550    558    652
576    575    648
570    574    641
560    554    640
558    551    650
561    551    666
624    638    683
564    581    647

-3778851060...

482    482    557
489    490    580
514    517    606
541    537    626
551    524    641
563    555    631
552    558    644
572    541    652
591    549    652
562    552    639

-3778851060...

482    482    560
507    503    591
525    543    596
555    531    609
553    556    634
540    552    640
579    598    635
607    554    639
588    585    679
547    560    643

-3778851060...

平均值:554 549 634

静态方法与 IL(Intermediate Language)比较 - IL 比静态方法快 1% (!) 尽管原因不明

静态方法与表达式树(Expression Tree)比较 - 静态方法比表达式树快 14%


编辑(2014年2月):我刚刚在.NET 4.5和更快的CPU上运行了上述代码(稍作修改),并得到了新的结果:方法/ET - 9%,方法/IL - 4%

因此,以前的结果已经不再有效 - 静态方法调用始终更快

*不确定是新硬件(i7-3820)还是新的.NET版本,或者我在旧测试中做错了什么*

另一个有趣的结果是,在 32位 下,完全相同的代码显示出三者之间没有任何差异

Method IL     ET    
--------------------
368    382    399
367    382    399
367    382    399
367    382    400
367    383    400
367    382    399
367    383    399
367    382    399
367    382    399
367    383    400
367    382    399
367    382    399
367    382    399
367    382    399
367    383    400
367    382    400
367    383    399
367    383    400
367    382    399
367    382    400

-7557702120...

--------------------
367.05 382.30 399.35

4
您应该创建并使用Compile()表达式树。

1
你能给我大致的想法吗?谢谢。 - Boppity Bop
@Bobb:在解析字符串后,使用Expression.*方法构建表达式树。http://msdn.microsoft.com/en-us/library/bb397951.aspx - SLaks
只要我理解正确,表达式需要先声明。例如:ExpressionTreeBuilder b = new ExpressionTreeBuilder(); Expression> e = b.GetExpressionTreeFromString("a + b - c"); var d = e.Compile(); var result = d(1, 2, 3);问题是:如何处理可变数量的参数? - Boppity Bop

1

使用CSharpCodeProvider时,Chris在几天内会遇到成百上千个dll文件的问题... 据我所知,每次调用它都会编译一个新的dll文件。这是正确的吗? - Boppity Bop
@Bobb - 对不起,我还没有完全测试过它。只是把它作为一个资源提供出来。 - Chris Gessler
不用了,谢谢。我知道CSharpCodeProvider,如果我找不到其他方法,我会尝试你的代码。只是我想找到更易管理的东西。谢谢。 - Boppity Bop

0

这取决于使用和优化。

如果你的测试不完美,基准测试可能会误导你。

你必须知道规则才能做到正确。

第一条规则

  • 静态方法可以在编译时进行优化并且可以内联。
  • IL 发射方法(DynamicMethod)在纯 IL 方面可能更快,因为你可以像你想要的那样进行优化(如果你能比标准优化器做得更好)。
  • 表达式树基于 DynamicMethod,但你无法手动优化它。

第二条规则

  • 性能由调用机制和方法的纯执行表示。
  • 使用委托调用方法意味着开销。
  • 内联抑制了调用机制。
  • DynamicMethod 只能内联到其他 DynamicMethod 中。
  • 大多数情况下,DynamicMethod 和 Expression Tree 都是使用委托调用的。
  • 委托到实例方法比委托到静态方法更快。

请记住:

  • 对于小型方法,静态方法通常更快。
  • 如果调用机制不是瓶颈,DynamicMethod 可能更快。
  • Expression Tree 不能比 DynamicMethod 更快,但有时(很少)可以比静态方法更快,这取决于你如何“表达它”。

结论:

性能取决于上下文。 如果可能的话,请继续使用静态方法。 如果优化器更改,性能可能会发生变化。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接