编译C#Lambda表达式性能

Hug*_*ira 91 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);
Run Code Online (Sandbox Code Playgroud)

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

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);
}
Run Code Online (Sandbox Code Playgroud)

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

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);
}
Run Code Online (Sandbox Code Playgroud)

当然它不完全像上面那样,所以公平地说,我稍微修改了第一个:

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);
}
Run Code Online (Sandbox Code Playgroud)

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

Using lambda compiled: 23437500
Using lambda:           1250000
Using lambda combined:  1406250
Run Code Online (Sandbox Code Playgroud)

调试关闭:

Using lambda compiled: 21718750
Using lambda:            937500
Using lambda combined:  1093750
Run Code Online (Sandbox Code Playgroud)

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

  1. 我在比较非等效表达式吗?
  2. 有没有一种机制让.NET"优化"编译的表达式?
  3. 如何以l.Where(i => i % 2 == 0).Where(i => i > 5);编程方式表达相同的链式调用?

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

Using lambda:           1093974
Using lambda compiled: 15315636
Using lambda combined:   781410
Run Code Online (Sandbox Code Playgroud)

调试ON,优化ON:

Using lambda:            781305
Using lambda compiled: 15469839
Using lambda combined:   468783
Run Code Online (Sandbox Code Playgroud)

调试OFF,优化ON:

Using lambda:            625020
Using lambda compiled: 14687970
Using lambda combined:   468765
Run Code Online (Sandbox Code Playgroud)

新惊喜.从VS2008(C#3)切换到VS2010(C#4),UsingLambdaCombined比原始lambda更快.


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

System.Reflection.Emit.DynamicMethod.CreateDelegate(class System.Type, object)
Run Code Online (Sandbox Code Playgroud)

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

Hug*_*ira 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);
    }
Run Code Online (Sandbox Code Playgroud)

现在的时间是:

Using lambda:                            625020
Using lambda compiled:                 14687970
Using lambda combined:                   468765
Using lambda compiled with MethodCall:   468765
Run Code Online (Sandbox Code Playgroud)

活泉!它不仅速度快,而且比原生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);
}
Run Code Online (Sandbox Code Playgroud)

还有一些时序,VS2010,Optimizations ON,Debugging OFF:

Using lambda:                            781260
Using lambda compiled:                 14687970
Using lambda combined:                   468756
Using lambda compiled with MethodCall:   468756
Using lambda compiled constant:          468756
Run Code Online (Sandbox Code Playgroud)

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


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

而且为什么,哦,为什么是这样的,现在比原来的拉姆达快!?


Jul*_*anR 10

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

编译到委托表达式的性能

对我来说,解决方案是我不应该叫CompileExpression,但我应该叫CompileToMethod上它和编译Expressionstatic方法在一个动态组装.

像这样:

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"));
Run Code Online (Sandbox Code Playgroud)

然而,这并不理想.我不是很肯定这适用于哪些类型的准确,但我认为这是作为参数通过委托,或返回由委托类型必须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;
  }
}
Run Code Online (Sandbox Code Playgroud)

但就像我说的那样,这并不理想,我仍然想知道为什么编译动态装配的方法有时会快一个数量级.我有时会说,因为我也看到过Expression编译的情况与Compile普通方法一样快的情况.看到我的问题.

或者如果有人知道绕过public动态组件的"无非类型"约束的方法,那也是受欢迎的.