编译C#Lambda表达式性能

时间:2011-04-06 14:45:55

标签: c# performance lambda expression-trees

考虑对集合进行以下简单操作:

static List<int> x = new List<int>() { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
var result = x.Where(i => i % 2 == 0).Where(i => i > 5);

现在让我们使用表达式。以下代码大致相当:

static void UsingLambda() {
    Func<IEnumerable<int>, IEnumerable<int>> lambda = l => l.Where(i => i % 2 == 0).Where(i => i > 5);
    var t0 = DateTime.Now.Ticks;
    for (int j = 1; j < MAX; j++) 
        var sss = lambda(x).ToList();

    var tn = DateTime.Now.Ticks;
    Console.WriteLine("Using lambda: {0}", tn - t0);
}

但是我想在运行中构建表达式,所以这是一个新的测试:

static void UsingCompiledExpression() {
    var f1 = (Expression<Func<IEnumerable<int>, IEnumerable<int>>>)(l => l.Where(i => i % 2 == 0));
    var f2 = (Expression<Func<IEnumerable<int>, IEnumerable<int>>>)(l => l.Where(i => i > 5));
    var argX = Expression.Parameter(typeof(IEnumerable<int>), "x");
    var f3 = Expression.Invoke(f2, Expression.Invoke(f1, argX));
    var f = Expression.Lambda<Func<IEnumerable<int>, IEnumerable<int>>>(f3, argX);

    var c3 = f.Compile();

    var t0 = DateTime.Now.Ticks;
    for (int j = 1; j < MAX; j++) 
        var sss = c3(x).ToList();

    var tn = DateTime.Now.Ticks;
    Console.WriteLine("Using lambda compiled: {0}", tn - t0);
}

当然它与上述不完全相同,所以公平地说,我稍微修改了第一个:

static void UsingLambdaCombined() {
    Func<IEnumerable<int>, IEnumerable<int>> f1 = l => l.Where(i => i % 2 == 0);
    Func<IEnumerable<int>, IEnumerable<int>> f2 = l => l.Where(i => i > 5);
    Func<IEnumerable<int>, IEnumerable<int>> lambdaCombined = l => f2(f1(l));
    var t0 = DateTime.Now.Ticks;
    for (int j = 1; j < MAX; j++) 
        var sss = lambdaCombined(x).ToList();

    var tn = DateTime.Now.Ticks;
    Console.WriteLine("Using lambda combined: {0}", tn - t0);
}

现在结果为MAX = 100000,VS2008,调试开启:

Using lambda compiled: 23437500
Using lambda:           1250000
Using lambda combined:  1406250

调试关闭:

Using lambda compiled: 21718750
Using lambda:            937500
Using lambda combined:  1093750

惊喜。编译后的表达式比其他替代方案慢大约17倍。现在问题出现了:

  1. 我是否在比较非等效表达式?
  2. 是否存在使.NET“优化”编译表达式的机制?
  3. 如何以编程方式表达相同的链式调用l.Where(i => i % 2 == 0).Where(i => i > 5);

  4. 更多统计数据。 Visual Studio 2010,调试ON,优化OFF:

    Using lambda:           1093974
    Using lambda compiled: 15315636
    Using lambda combined:   781410
    

    调试ON,优化ON:

    Using lambda:            781305
    Using lambda compiled: 15469839
    Using lambda combined:   468783
    

    调试OFF,优化ON:

    Using lambda:            625020
    Using lambda compiled: 14687970
    Using lambda combined:   468765
    

    新的惊喜。从VS2008(C#3)切换到VS2010(C#4),使UsingLambdaCombined比原生lambda更快。


    好的,我找到了一种方法来将lambda编译的性能提高一个数量级以上。这是一个提示;运行探查器后,92%的时间用于:

    System.Reflection.Emit.DynamicMethod.CreateDelegate(class System.Type, object)
    

    嗯...为什么在每次迭代中都会创建一个新的委托?我不确定,但解决方案是在一个单独的帖子中。

4 个答案:

答案 0 :(得分:43)

可能是内部lambda没有被编译?!?这是一个概念证明:

static void UsingCompiledExpressionWithMethodCall() {
        var where = typeof(Enumerable).GetMember("Where").First() as System.Reflection.MethodInfo;
        where = where.MakeGenericMethod(typeof(int));
        var l = Expression.Parameter(typeof(IEnumerable<int>), "l");
        var arg0 = Expression.Parameter(typeof(int), "i");
        var lambda0 = Expression.Lambda<Func<int, bool>>(
            Expression.Equal(Expression.Modulo(arg0, Expression.Constant(2)),
                             Expression.Constant(0)), arg0).Compile();
        var c1 = Expression.Call(where, l, Expression.Constant(lambda0));
        var arg1 = Expression.Parameter(typeof(int), "i");
        var lambda1 = Expression.Lambda<Func<int, bool>>(Expression.GreaterThan(arg1, Expression.Constant(5)), arg1).Compile();
        var c2 = Expression.Call(where, c1, Expression.Constant(lambda1));

        var f = Expression.Lambda<Func<IEnumerable<int>, IEnumerable<int>>>(c2, l);

        var c3 = f.Compile();

        var t0 = DateTime.Now.Ticks;
        for (int j = 1; j < MAX; j++)
        {
            var sss = c3(x).ToList();
        }

        var tn = DateTime.Now.Ticks;
        Console.WriteLine("Using lambda compiled with MethodCall: {0}", tn - t0);
    }

现在的时间是:

Using lambda:                            625020
Using lambda compiled:                 14687970
Using lambda combined:                   468765
Using lambda compiled with MethodCall:   468765

活泉!它不仅速度快,而且比原生lambda快。 ( Scratch head )。


当然上面的代码太难以写了。让我们做一些简单的魔术:

static void UsingCompiledConstantExpressions() {
    var f1 = (Func<IEnumerable<int>, IEnumerable<int>>)(l => l.Where(i => i % 2 == 0));
    var f2 = (Func<IEnumerable<int>, IEnumerable<int>>)(l => l.Where(i => i > 5));
    var argX = Expression.Parameter(typeof(IEnumerable<int>), "x");
    var f3 = Expression.Invoke(Expression.Constant(f2), Expression.Invoke(Expression.Constant(f1), argX));
    var f = Expression.Lambda<Func<IEnumerable<int>, IEnumerable<int>>>(f3, argX);

    var c3 = f.Compile();

    var t0 = DateTime.Now.Ticks;
    for (int j = 1; j < MAX; j++) {
        var sss = c3(x).ToList();
    }

    var tn = DateTime.Now.Ticks;
    Console.WriteLine("Using lambda compiled constant: {0}", tn - t0);
}

和一些时间,VS2010,优化开启,调试关闭:

Using lambda:                            781260
Using lambda compiled:                 14687970
Using lambda combined:                   468756
Using lambda compiled with MethodCall:   468756
Using lambda compiled constant:          468756

现在你可以争辩说我没有动态生成整个表达式;只是链接调用。但在上面的例子中,我生成了整个表达式。时间匹配。这只是编写更少代码的捷径。


根据我的理解,正在发生的是.Compile()方法不会将编译传播到内部lambdas,因此不会将CreateDelegate的常量调用传播。但要真正理解这一点,我希望有一个.NET大师对内部事情进行一些评论。

为什么,哦为什么现在比现在的lambda更快了??

答案 1 :(得分:10)

最近我问了一个几乎相同的问题:

Performance of compiled-to-delegate Expression

我的解决方案是,我不应该在Compile上致电Expression,但我应该在其上调用CompileToMethod并将Expression编译为{动态程序集中的{1}}方法。

像这样:

static

然而,这并不理想。我不太确定这适用于哪种类型,但我认为由委托作为参数或由委托返回的类型var assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly( new AssemblyName("MyAssembly_" + Guid.NewGuid().ToString("N")), AssemblyBuilderAccess.Run); var moduleBuilder = assemblyBuilder.DefineDynamicModule("Module"); var typeBuilder = moduleBuilder.DefineType("MyType_" + Guid.NewGuid().ToString("N"), TypeAttributes.Public)); var methodBuilder = typeBuilder.DefineMethod("MyMethod", MethodAttributes.Public | MethodAttributes.Static); expression.CompileToMethod(methodBuilder); var resultingType = typeBuilder.CreateType(); var function = Delegate.CreateDelegate(expression.Type, resultingType.GetMethod("MyMethod")); 而非通用的。它必须是非泛型的,因为泛型类型显然访问public,这是.NET在泛型类型下使用的内部类型,这违反了“必须是System.__Canon类型规则”。

对于这些类型,您可以使用明显较慢的public。我用以下方式检测它们:

Compile

但就像我说的那样,这并不理想,我仍然想知道为什么将动态装配的方法编译为有时要快一个数量级。我有时会说,因为我也看到了用private static bool IsPublicType(Type t) { if ((!t.IsPublic && !t.IsNestedPublic) || t.IsGenericType) { return false; } int lastIndex = t.FullName.LastIndexOf('+'); if (lastIndex > 0) { var containgTypeName = t.FullName.Substring(0, lastIndex); var containingType = Type.GetType(containgTypeName + "," + t.Assembly); if (containingType != null) { return containingType.IsPublic; } return false; } else { return t.IsPublic; } } 编译的Expression与普通方法一样快的情况。看到我的问题。

或者,如果有人知道绕过动态装配的“无非Compile类型”约束的方法,那也是受欢迎的。

答案 2 :(得分:4)

您的表达方式不相同,因此您会得到偏差的结果。我写了一个测试台来测试这个。测试包括常规的lambda调用,等效的编译表达式,手工制作的等效编译表达式,以及组合版本。这些应该是更准确的数字。有趣的是,我没有看到普通版本和合成版本之间存在太大差异。编译后的表达式自然会慢一点,但只是很少。您需要足够大的输入和迭代计数才能获得一些好的数字。它有所作为。

至于你的第二个问题,我不知道你怎么能从中获得更多的表现,所以我无法帮助你。它看起来和它一样好。

您可以在HandMadeLambdaExpression()方法中找到我对第三个问题的答案。由于扩展方法,不是最容易构建的表达式,但可行。

using System;
using System.Collections.Generic;
using System.Linq;

using System.Diagnostics;
using System.Linq.Expressions;

namespace ExpressionBench
{
    class Program
    {
        static void Main(string[] args)
        {
            var values = Enumerable.Range(0, 5000);
            var lambda = GetLambda();
            var lambdaExpression = GetLambdaExpression().Compile();
            var handMadeLambdaExpression = GetHandMadeLambdaExpression().Compile();
            var composed = GetComposed();
            var composedExpression = GetComposedExpression().Compile();
            var handMadeComposedExpression = GetHandMadeComposedExpression().Compile();

            DoTest("Lambda", values, lambda);
            DoTest("Lambda Expression", values, lambdaExpression);
            DoTest("Hand Made Lambda Expression", values, handMadeLambdaExpression);
            Console.WriteLine();
            DoTest("Composed", values, composed);
            DoTest("Composed Expression", values, composedExpression);
            DoTest("Hand Made Composed Expression", values, handMadeComposedExpression);
        }

        static void DoTest<TInput, TOutput>(string name, TInput sequence, Func<TInput, TOutput> operation, int count = 1000000)
        {
            for (int _ = 0; _ < 1000; _++)
                operation(sequence);
            var sw = Stopwatch.StartNew();
            for (int _ = 0; _ < count; _++)
                operation(sequence);
            sw.Stop();
            Console.WriteLine("{0}:", name);
            Console.WriteLine("  Elapsed: {0,10} {1,10} (ms)", sw.ElapsedTicks, sw.ElapsedMilliseconds);
            Console.WriteLine("  Average: {0,10} {1,10} (ms)", decimal.Divide(sw.ElapsedTicks, count), decimal.Divide(sw.ElapsedMilliseconds, count));
        }

        static Func<IEnumerable<int>, IList<int>> GetLambda()
        {
            return v => v.Where(i => i % 2 == 0).Where(i => i > 5).ToList();
        }

        static Expression<Func<IEnumerable<int>, IList<int>>> GetLambdaExpression()
        {
            return v => v.Where(i => i % 2 == 0).Where(i => i > 5).ToList();
        }

        static Expression<Func<IEnumerable<int>, IList<int>>> GetHandMadeLambdaExpression()
        {
            var enumerableMethods = typeof(Enumerable).GetMethods();
            var whereMethod = enumerableMethods
                .Where(m => m.Name == "Where")
                .Select(m => m.MakeGenericMethod(typeof(int)))
                .Where(m => m.GetParameters()[1].ParameterType == typeof(Func<int, bool>))
                .Single();
            var toListMethod = enumerableMethods
                .Where(m => m.Name == "ToList")
                .Select(m => m.MakeGenericMethod(typeof(int)))
                .Single();

            // helpers to create the static method call expressions
            Func<Expression, ParameterExpression, Func<ParameterExpression, Expression>, Expression> WhereExpression =
                (instance, param, body) => Expression.Call(whereMethod, instance, Expression.Lambda(body(param), param));
            Func<Expression, Expression> ToListExpression =
                instance => Expression.Call(toListMethod, instance);

            //return v => v.Where(i => i % 2 == 0).Where(i => i > 5).ToList();
            var exprParam = Expression.Parameter(typeof(IEnumerable<int>), "v");
            var expr0 = WhereExpression(exprParam,
                Expression.Parameter(typeof(int), "i"),
                i => Expression.Equal(Expression.Modulo(i, Expression.Constant(2)), Expression.Constant(0)));
            var expr1 = WhereExpression(expr0,
                Expression.Parameter(typeof(int), "i"),
                i => Expression.GreaterThan(i, Expression.Constant(5)));
            var exprBody = ToListExpression(expr1);
            return Expression.Lambda<Func<IEnumerable<int>, IList<int>>>(exprBody, exprParam);
        }

        static Func<IEnumerable<int>, IList<int>> GetComposed()
        {
            Func<IEnumerable<int>, IEnumerable<int>> composed0 =
                v => v.Where(i => i % 2 == 0);
            Func<IEnumerable<int>, IEnumerable<int>> composed1 =
                v => v.Where(i => i > 5);
            Func<IEnumerable<int>, IList<int>> composed2 =
                v => v.ToList();
            return v => composed2(composed1(composed0(v)));
        }

        static Expression<Func<IEnumerable<int>, IList<int>>> GetComposedExpression()
        {
            Expression<Func<IEnumerable<int>, IEnumerable<int>>> composed0 =
                v => v.Where(i => i % 2 == 0);
            Expression<Func<IEnumerable<int>, IEnumerable<int>>> composed1 =
                v => v.Where(i => i > 5);
            Expression<Func<IEnumerable<int>, IList<int>>> composed2 =
                v => v.ToList();
            var exprParam = Expression.Parameter(typeof(IEnumerable<int>), "v");
            var exprBody = Expression.Invoke(composed2, Expression.Invoke(composed1, Expression.Invoke(composed0, exprParam)));
            return Expression.Lambda<Func<IEnumerable<int>, IList<int>>>(exprBody, exprParam);
        }

        static Expression<Func<IEnumerable<int>, IList<int>>> GetHandMadeComposedExpression()
        {
            var enumerableMethods = typeof(Enumerable).GetMethods();
            var whereMethod = enumerableMethods
                .Where(m => m.Name == "Where")
                .Select(m => m.MakeGenericMethod(typeof(int)))
                .Where(m => m.GetParameters()[1].ParameterType == typeof(Func<int, bool>))
                .Single();
            var toListMethod = enumerableMethods
                .Where(m => m.Name == "ToList")
                .Select(m => m.MakeGenericMethod(typeof(int)))
                .Single();

            Func<ParameterExpression, Func<ParameterExpression, Expression>, Expression> LambdaExpression =
                (param, body) => Expression.Lambda(body(param), param);
            Func<Expression, ParameterExpression, Func<ParameterExpression, Expression>, Expression> WhereExpression =
                (instance, param, body) => Expression.Call(whereMethod, instance, Expression.Lambda(body(param), param));
            Func<Expression, Expression> ToListExpression =
                instance => Expression.Call(toListMethod, instance);

            var composed0 = LambdaExpression(Expression.Parameter(typeof(IEnumerable<int>), "v"),
                v => WhereExpression(
                    v,
                    Expression.Parameter(typeof(int), "i"),
                    i => Expression.Equal(Expression.Modulo(i, Expression.Constant(2)), Expression.Constant(0))));
            var composed1 = LambdaExpression(Expression.Parameter(typeof(IEnumerable<int>), "v"),
                v => WhereExpression(
                    v,
                    Expression.Parameter(typeof(int), "i"),
                    i => Expression.GreaterThan(i, Expression.Constant(5))));
            var composed2 = LambdaExpression(Expression.Parameter(typeof(IEnumerable<int>), "v"),
                v => ToListExpression(v));

            var exprParam = Expression.Parameter(typeof(IEnumerable<int>), "v");
            var exprBody = Expression.Invoke(composed2, Expression.Invoke(composed1, Expression.Invoke(composed0, exprParam)));
            return Expression.Lambda<Func<IEnumerable<int>, IList<int>>>(exprBody, exprParam);
        }
    }
}

我机器上的结果:

Lambda:
  Elapsed:  340971948     123230 (ms)
  Average: 340.971948    0.12323 (ms)
Lambda Expression:
  Elapsed:  357077202     129051 (ms)
  Average: 357.077202   0.129051 (ms)
Hand Made Lambda Expression:
  Elapsed:  345029281     124696 (ms)
  Average: 345.029281   0.124696 (ms)

Composed:
  Elapsed:  340409238     123027 (ms)
  Average: 340.409238   0.123027 (ms)
Composed Expression:
  Elapsed:  350800599     126782 (ms)
  Average: 350.800599   0.126782 (ms)
Hand Made Composed Expression:
  Elapsed:  352811359     127509 (ms)
  Average: 352.811359   0.127509 (ms)

答案 3 :(得分:3)

编译的lambda性能超过委托可能会慢一些,因为运行时的编译代码可能没有优化,但是手动编写的代码和通过C#编译器编译的代码已经过优化。

其次,多个lambda表达式意味着多个匿名方法,并且调用每个方法只需要花费额外的时间来评估直接方法。例如,调用

Console.WriteLine(x);

Action x => Console.WriteLine(x);
x(); // this means two different calls..

是不同的,从编译器的角度来看,第二个需要更多的开销,实际上是两个不同的调用。首先调用x本身然后调用x的语句。

所以你的组合Lambda肯定会比单个lambda表达式的性能慢一点。

这与内部执行的内容无关,因为您仍在评估正确的逻辑,但是您正在为编译器添加额外的步骤。

即使在表达式树被编译之后,它也不会有优化,它仍然会保留其复杂的小结构,评估和调用它可能会有额外的验证,空检查等,这可能会降低编译的lambda表达式的性能。 / p>