【问题标题】:Compiled C# Lambda Expressions Performance编译的 C# Lambda 表达式性能
【发布时间】:2011-07-30 21:51:47
【问题描述】:

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

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,调试 ON 的结果:

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 =&gt; i % 2 == 0).Where(i =&gt; i &gt; 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)

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

【问题讨论】:

  • 这些时间是在 Visual Studio 中运行的吗?如果是这样,请使用发布模式构建重复计时并在不调试的情况下运行(即在 Visual Studio 中或从命令行中使用 Ctrl+F5)。另外,请考虑使用Stopwatch 而非DateTime.Now 进行计时。
  • 我不知道为什么它比较慢,但是你的基准测试技术不是很好。首先, DateTime.Now 仅精确到 1/64 秒,因此您的测量舍入误差很大。改用秒表;它精确到几纳秒。其次,您正在测量 jit 代码(第一次调用)和每个后续调用的时间;这可以甩掉平均值。 (尽管在这种情况下,十万的 MAX 可能足以平均化 jit 负担,但将其包含在平均值中仍然是一种不好的做法。)
  • @Eric,只有在每个操作中使用 DateTime.Now.Ticks 时才会出现舍入误差,在开始之前和结束之后,毫秒计数足够高以显示性能差异。
  • 如果使用秒表,我建议阅读这篇文章以确保准确的结果:codeproject.com/KB/testing/stopwatch-measure-precise.aspx
  • @Eric,虽然我同意这不是最精确的测量技术,但我们谈论的是一个数量级的差异。 MAX 足够高,可以减少明显的偏差。

标签: c# performance lambda expression-trees


【解决方案1】:

会不会是内部的 lambdas 没有被编译?!?这是一个概念证明:

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 更快!?

【讨论】:

  • 我正在考虑接受我自己的答案,因为这是得票最多的答案。我应该再等一会儿吗?
  • 关于你获得比原生 lambda 更快的代码会发生什么,你可能想看看这个关于微基准的页面(它没有任何真正的 Java 特定,尽管名称):code.google.com/p/caliper/wiki/JavaMicrobenchmarks
  • 至于为什么动态编译的 lambda 更快,我怀疑“使用 lambda”,首先运行,会因为必须 JIT 一些代码而受到惩罚。
  • 我不知道发生了什么,有一次当我测试编译后的表达式和 createdelegate 以设置和获取字段和属性时,createdelegate 的属性要快得多,但编译的字段要快一些
【解决方案2】:

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

Performance of compiled-to-delegate Expression

我的解决方案是我不应该在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"));

但这并不理想。我不太确定这究竟适用于哪些类型,但我认为委托作为参数或委托返回的类型具有public 和非泛型。它必须是非泛型的,因为泛型类型显然访问 System.__Canon,这是 .NET 用于泛型类型的内部类型,这违反了“必须是 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;
  }
}

但就像我说的那样,这并不理想,我仍然想知道为什么将方法编译为动态程序集有时要快一个数量级。我有时会这么说是因为我也见过用Compile 编译的Expression 与普通方法一样快的情况。请参阅我的问题。

或者,如果有人知道使用动态程序集绕过“无非public 类型”约束的方法,那也是受欢迎的。

【讨论】:

    【解决方案3】:

    您的表达式不等价,因此您会得到不正确的结果。我写了一个测试台来测试这个。测试包括常规 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);
            }
        }
    }
    

    以及我机器上的结果:

    拉姆达: 已用:340971948 123230(毫秒) 平均:340.971948 0.12323(毫秒) 拉姆达表达式: 已用: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(毫秒)

    【讨论】:

      【解决方案4】:

      相对于委托的编译 lambda 性能可能会更慢,因为在运行时编译的代码可能不会被优化,但是您手动编写的代码和通过 C# 编译器编译的代码会被优化。

      第二,多个 lambda 表达式意味着多个匿名方法,调用它们中的每一个都比评估直接方法花费的时间很少。例如,调用

      Console.WriteLine(x);
      

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

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

      因此,与单个 lambda 表达式相比,您的组合 Lambda 肯定不会有什么缓慢的性能。

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

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

      【讨论】:

      • 如果仔细看,UsingLambdaCombined 测试是结合了多个 lambda 函数,其性能非常接近UsingLambda。关于优化,我确信它们是由 JIT 引擎处理的,因此运行时生成的代码(编译后)也将成为任何 JIT 优化的目标。
      • JIT 优化和编译时优化是两个不同的东西,您可以在项目设置中关闭编译时优化。其次,表达式编译可能会发出动态 MSIL,它会再慢一点,因为它的逻辑和操作顺序将根据需要包含空检查和有效性。你可以在反射器中查看它是如何编译的。
      • 虽然你的推理是合理的,但我不得不在这个特定问题上不同意你的观点(即数量级差异不是由于静态编译造成的)。首先,因为如果您实际上禁用了编译时优化,差异仍然很大。其次,因为我已经找到了一种方法来优化动态生成,使其稍微慢一点。让我试着理解“为什么”,然后我会发布结果。
      猜你喜欢
      • 2011-12-24
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-06-30
      • 1970-01-01
      • 2017-01-06
      相关资源
      最近更新 更多