编译的C# Lambda表达式性能

91

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

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);

更多统计信息。Visual Studio 2010,开启调试,关闭优化:

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

调试开启,优化开启:

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

关闭调试,开启优化:

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)

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


3
这些时间是在Visual Studio中运行的吗?如果是,那么请使用发布模式构建并在不调试的情况下运行计时(即在Visual Studio中按Ctrl + F5或从命令行中运行)。此外,考虑使用Stopwatch而不是DateTime.Now进行计时。 - Jim Mischel
12
我不知道为什么速度会变慢,但是你的基准测试技术并不是很好。首先,DateTime.Now只能精确到1/64秒,所以你的测量舍入误差很大。改用Stopwatch吧,它可以精确到几个纳秒。其次,你同时测量了代码JIT的时间(第一次调用)和每个后续调用的时间;这可能会影响平均值。(尽管在本例中,最多一百万次循环可能足以平均掉JIT负担,但是将其包含在平均值中是一个不好的做法。) - Eric Lippert
7
@Eric,只有在每个操作中都使用了DateTime.Now.Ticks之后,在开始和结束前,毫秒计数足够高以显示性能差异时,才会存在舍入误差。请注意不要改变原意,使语言通俗易懂。 - Akash Kava
1
如果使用秒表,我建议按照这篇文章来确保准确的结果:http://www.codeproject.com/KB/testing/stopwatch-measure-precise.aspx - Zach Green
1
@Eric,虽然我同意这不是最精确的测量技术,但我们谈论的是数量级的差异。MAX足够高,可以减少显著的偏差。 - Hugo Sereno Ferreira
显示剩余2条评论
4个回答

45

内部的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更快。(扯头皮)。


当然,上面的代码太痛苦了,让我们进行一些简单的魔法:

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()方法不会将编译传播到内部lambda,因此常量CreateDelegate被反复调用。但要真正理解这一点,我希望有一个.NET专家对内部内容进行一些评论。

而且,为什么这比本机lambda还要快呢?


1
我在考虑接受自己的答案,因为它是得票最多的一个。我应该再等一会儿吗? - Hugo Sereno Ferreira
关于您获取代码比本地lambda更快的情况,您可能想查看一下这个关于微基准测试的页面(尽管名称不是Java特定的): http://code.google.com/p/caliper/wiki/JavaMicrobenchmarks - Blaisorblade
至于为什么动态编译的 lambda 更快,我猜测“使用 lambda”的代码首先被运行,因此需要 JIT 编译一些代码而受到惩罚。 - Oskar Berggren
我不知道发生了什么,有一次当我测试编译表达式并创建委托来设置和获取字段和属性时,对于属性来说,创建的委托要快得多,但是对于字段来说,编译的速度略微更快。 - nawfal

11

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

编译为委托表达式的性能

对于我来说,解决方案是不应该在Expression上调用Compile,而是应该在其中调用CompileToMethod,并将Expression编译为动态程序集中的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"));

这并不是理想的情况。我不确定确切适用于哪些类型,但我认为作为委托参数或由委托返回的类型必须是公共的和非泛型的。它必须是非泛型的,因为泛型类型显然访问.NET底层使用的内部类型System.__Canon,这违反了“必须是公共类型规则”。对于这些类型,您可以使用明显较慢的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;
  }
}

但是,就像我说的那样,这并不理想。我仍然想知道为什么将方法编译为动态程序集有时快上一个数量级。我说有时是因为我也见过使用Compile编译的表达式和普通方法一样快的情况。请参阅我的问题。
或者,如果有人知道如何绕过动态程序集中“无非公共类型”限制的方法,也可以提供。

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:
  耗时: 340971948     123230 (毫秒)
  平均值: 340.971948    0.12323 (毫秒)
Lambda表达式:
  耗时: 357077202     129051 (毫秒)
  平均值: 357.077202   0.129051 (毫秒)
手写Lambda表达式:
  耗时: 345029281     124696 (毫秒)
  平均值: 345.029281   0.124696 (毫秒)
组合: 耗时: 340409238 123027 (毫秒) 平均值: 340.409238 0.123027 (毫秒) 组合表达式: 耗时: 350800599 126782 (毫秒) 平均值: 350.800599 0.126782 (毫秒) 手写组合表达式: 耗时: 352811359 127509 (毫秒) 平均值: 352.811359 0.127509 (毫秒)

3

编译后的lambda表达式性能可能会比委托慢,因为运行时的编译代码可能没有被优化,但是你手写的代码和通过C#编译器编译的代码是经过优化的。

其次,多个lambda表达式意味着多个匿名方法,而调用它们中的每一个需要比评估直接方法多一点时间。例如,调用

Console.WriteLine(x);

and

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

这两种方法是不同的,使用第二种方法需要更多的开销,因为从编译器的角度来看,实际上是两个不同的调用。首先调用 x 本身,然后在其中调用 x 的语句。

因此,你的组合 Lambda 表达式肯定比单个 Lambda 表达式慢一点。

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

即使表达式树被编译后,它也不会进行优化,它仍然保留其略微复杂的结构,评估和调用它可能需要额外的验证、空值检查等,这可能会减慢编译 Lambda 表达式的性能。


2
如果你仔细看,UsingLambdaCombined 测试正在组合多个 lambda 函数,其性能非常接近 UsingLambda。关于优化,我相信它们是由 JIT 引擎处理的,因此运行时生成的代码(编译后)也将成为任何 JIT 优化的目标。 - Hugo Sereno Ferreira
1
JIT 优化和编译时优化是两个不同的概念,您可以在项目设置中关闭编译时优化。其次,表达式编译可能会发出动态 MSIL,这将会比较慢,因为它的逻辑和操作序列将包含空值检查和有效性检查。您可以查看反编译器以了解其编译方式。 - Akash Kava
2
虽然你的推理是正确的,但在这个特定的问题上我不同意你的观点(即,数量级差异并非由静态编译引起)。首先,因为如果你真的禁用了编译时优化,差异仍然相当大。其次,因为我已经找到了一种优化动态生成代码的方法,只是稍微慢一些而已。让我试着弄清楚“为什么”,然后我会发布结果。 - Hugo Sereno Ferreira

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