表达式树的性能

本文关键字:性能 表达式 | 更新日期: 2023-09-27 18:13:16

我目前的理解是像这样的'硬编码'代码:

public int Add(int x, int y) {return x + y;}

总是比下面这样的表达式树代码执行得更好:

Expression<Func<int, int, int>> add = (x, y) => x + y; 
var result = add.Compile()(2, 3);
var x = Expression.Parameter(typeof(int)); 
var y = Expression.Parameter(typeof(int)); 
return (Expression.Lambda(Expression.Add(x, y), x, y).
    Compile() as Func<int, int, int>)(2, 3);

,因为编译器有更多的信息,如果在编译时编译,可以花更多的精力来优化代码。这通常是正确的吗?

表达式树的性能

编译

Expression.Compile的调用与应用程序包含的任何其他。net代码的调用过程完全相同,在某种意义上:

  • 生成IL代码
  • IL代码被编译为机器码

(跳过解析步骤,因为表达式树已经创建,不需要从输入代码生成)

您可以查看表达式编译器的源代码,以验证是否确实生成了IL代码。

请注意,CLR所做的几乎所有优化都是在JIT步骤中完成的,而不是在编译c#源代码时完成的。当从lambda委托编译IL代码为机器代码时,也会进行此优化。

<<p> 你的例子/strong>

在你的例子中,你正在比较苹果&橘子。第一个例子是方法定义,第二个例子是创建方法、编译和执行方法的运行时代码。创建/编译方法所花费的时间比实际执行它的时间要长得多。但是,您可以在创建后保留已编译方法的实例。当你这样做了,你所生成的方法的性能应该与原始c#方法的性能相同。

考虑这种情况:

private static int AddMethod(int a, int b)
{
    return a + b;
}
Func<int, int, int> add1 = (a, b) => a + b;
Func<int, int, int> add2 = AddMethod;
var x = Expression.Parameter(typeof (int));
var y = Expression.Parameter(typeof (int));
var additionExpr = Expression.Add(x, y);
Func<int, int, int> add3 = 
              Expression.Lambda<Func<int, int, int>>(
                  additionExpr, x, y).Compile();
//the above steps cost a lot of time, relatively.
//performance of these three should be identical
add1(1, 2);
add2(1, 2);
add3(1, 2);

因此可以得出结论:IL代码就是IL代码,不管它是如何生成的,而Linq Expressions生成IL代码。

您的Add函数可能编译成一些函数开销(如果没有内联)和单个add指令。没有比这更快的了

即使构造这个表达式树也会慢几个数量级。与直接的c#实现相比,为每次调用编译一个新函数将会非常昂贵。

尝试只编译一次函数并将其存储在某个地方

试图理解为什么我的构建和编译lambda运行稍微慢于"只是委托"(我想我需要为它创建新的SO问题),我发现了这个线程,并决定使用BenchmarkDotNet检查性能。让我惊讶的是:手动构建和编译lambda是最快的。是的,方法之间存在稳定的差异。

结果:

BenchmarkDotNet=v0.10.5, OS=Windows 10.0.14393
Processor=Intel Core i5-2500K CPU 3.30GHz (Sandy Bridge), ProcessorCount=4
Frequency=3233539 Hz, Resolution=309.2587 ns, Timer=TSC
  [Host] : Clr 4.0.30319.42000, 64bit RyuJIT-v4.6.1648.0
  Clr    : Clr 4.0.30319.42000, 64bit RyuJIT-v4.6.1648.0
  Core   : .NET Core 4.6.25009.03, 64bit RyuJIT

         Method |  Job | Runtime |      Mean |     Error |    StdDev |    Median |       Min |        Max | Rank | Allocated |
--------------- |----- |-------- |----------:|----------:|----------:|----------:|----------:|-----------:|-----:|----------:|
     AddBuilded |  Clr |     Clr | 0.8826 ns | 0.0278 ns | 0.0232 ns | 0.8913 ns | 0.8429 ns |  0.9195 ns |    1 |       0 B |
      AddLambda |  Clr |     Clr | 1.5077 ns | 0.0226 ns | 0.0212 ns | 1.4986 ns | 1.4769 ns |  1.5395 ns |    2 |       0 B |
 AddLambdaConst |  Clr |     Clr | 6.4535 ns | 0.0454 ns | 0.0425 ns | 6.4439 ns | 6.4030 ns |  6.5323 ns |    3 |       0 B |
     AddBuilded | Core |    Core | 0.8993 ns | 0.0249 ns | 0.0233 ns | 0.8908 ns | 0.8777 ns |  0.9506 ns |    1 |       0 B |
      AddLambda | Core |    Core | 1.5105 ns | 0.0241 ns | 0.0201 ns | 1.5094 ns | 1.4731 ns |  1.5396 ns |    2 |       0 B |
 AddLambdaConst | Core |    Core | 9.3849 ns | 0.2237 ns | 0.5693 ns | 9.6577 ns | 8.3455 ns | 10.0590 ns |    4 |       0 B |

我不能从中得出任何结论,这可能是IL代码或JIT编译器影响的差异。

代码:

    static BenchmarkLambdaSimple()
    {
        addLambda = (a, b) => a + b;
        addLambdaConst = AddMethod;
        var x = Expression.Parameter(typeof(int));
        var y = Expression.Parameter(typeof(int));
        var additionExpr = Expression.Add(x, y);
        addBuilded =
                      Expression.Lambda<Func<int, int, int>>(
                          additionExpr, x, y).Compile();
    }
    static Func<int, int, int> addLambda;
    static Func<int, int, int> addLambdaConst;
    static Func<int, int, int> addBuilded;
    private static int AddMethod(int a, int b)
    {
        return a + b;
    }
    [Benchmark]
    public int AddBuilded()
    {
        return addBuilded(1, 2);
    }
    [Benchmark]
    public int AddLambda()
    {
        return addLambda(1, 2);
    }
    [Benchmark]
    public int AddLambdaConst()
    {
        return addLambdaConst(1, 2);
    }

好吧,我写了一个小测试(可能需要你的专家仔细检查),但似乎表达式树是最快的(add3),其次是add2,然后是add1!

using System;
using System.Diagnostics;
using System.Linq.Expressions;
namespace ExpressionTreeTest
{
    class Program
    {
        static void Main()
        {
            Func<int, int, int> add1 = (a, b) => a + b;
            Func<int, int, int> add2 = AddMethod;
            var x = Expression.Parameter(typeof(int));
            var y = Expression.Parameter(typeof(int));
            var additionExpr = Expression.Add(x, y);
            var add3 = Expression.Lambda<Func<int, int, int>>(
                              additionExpr, x, y).Compile();

            TimingTest(add1, "add1", 1000000);
            TimingTest(add2, "add2", 1000000);
            TimingTest(add3, "add3", 1000000);
        }
        private static void TimingTest(Func<int, int, int> addMethod, string testMethod, int numberOfAdditions)
        {
            var sw = new Stopwatch();
            sw.Start();
            for (var c = 0; c < numberOfAdditions; c++)
            {
               addMethod(1, 2);              
            }
            sw.Stop();
           Console.WriteLine("Total calculation time {1}: {0}", sw.Elapsed, testMethod);
        }
        private static int AddMethod(int a, int b)
        {
            return a + b;
        }
    }
}

My results debug mode:

Total calculation time add1: 00:00:00.0134612
Total calculation time add2: 00:00:00.0133916
Total calculation time add3: 00:00:00.0053629

我的结果发布模式:

Total calculation time add1: 00:00:00.0026172
Total calculation time add2: 00:00:00.0027046
Total calculation time add3: 00:00:00.0014334

c# 6.0现在允许这样做:

public int Add(int x, int y) => x + y;

代替:

public int Add(int x, int y) {return x + y;}

参见方法表达式和属性表达式