Search code examples
c#linqlambdaexpression-trees

When compiling a LambdaExpression with nested LambdaExpressions, will they compile also?


When building a LambdaExpression at runtime, if I use a LambdaExpression as a parameter to a call expression (like when using Linq), then compile the main lambda, does the nested lambda also compile or does it need to?

The code functions the same if I use the LambdaExpression as a parameter to a method taking Func<T, T2> or if I compile it and use a Expression.Constant over the compiled Func.

Not compiled:

    var selectParam = Expression.Parameter(propType, "selectParam");
    var selectExp = Expression.Call(typeof(System.Linq.Enumerable).GetMethods().First(a => a.Name == "Select" && /*Func<TSource,TResult>*/ a.GetParameters().Last().ParameterType.GenericTypeArguments.Length == 2).MakeGenericMethod(propType, typeof(int)),
                                    whereExp,
                                    Expression.Lambda(Expression.Property(selectParam, "Length"), selectParam));

Compiled:

    var selectParam = Expression.Parameter(propType, "selectParam");
    var selectExp = Expression.Call(typeof(System.Linq.Enumerable).GetMethods().First(a => a.Name == "Select" && /*Func<TSource,TResult>*/ a.GetParameters().Last().ParameterType.GenericTypeArguments.Length == 2).MakeGenericMethod(propType, typeof(int)),
                                    whereExp,
                                    Expression.Constant(Expression.Lambda(Expression.Property(selectParam, "Length"), selectParam).Compile())); //compile

The expressions I'm building are called millions of times in a loop so I'd like to know if compiling the outer lambda compiles the inner lambdas properly.

Since this isn't easy to explain, see my fiddle here.

I'm pretty sure they won't be compiled as the methods being called could want them as Expressions to parse them. In this case, is there a runtime performance gain to compile them when used like this?

Thinking at a higher level, When used in a standard way in a loop - is this optimized at all? Certainly they aren't compiled on every call when doing linq over an array or such?


Solution

  • Short answer: yes, each inner lambda will be compiled.


    I slightly modified your first method (but it generates same expression):

    private static Expression<Func<int>> ActuallyInnerAlsoCompile()
    {
        var strType = typeof(string);
        var intType = typeof(int);
        var enumearbleType = typeof(Enumerable);
    
        var array = Expression.NewArrayInit(strType, Expression.Constant("test"), Expression.Constant("test2"));
    
        var x = Expression.Parameter(strType, "whereParam");
        var whereExp = Expression.Call(enumearbleType,
            "Where",
            new[] {strType},
            array,
            Expression.Lambda(Expression.NotEqual(Expression.PropertyOrField(x, "Length"), Expression.Constant(4)), x));
    
        var selectExp = Expression.Call(enumearbleType,
            "Select",
            new[] {strType, intType},
            whereExp,
            Expression.Lambda(Expression.PropertyOrField(x, "Length"), x));
    
        var firstOrDefault = Expression.Call(enumearbleType,
            "FirstOrDefault",
            new[] {intType},
            selectExp);
    
        return Expression.Lambda<Func<int>>(firstOrDefault);
    }
    

    Now you can refer to this answer and compile your expression into new assembly:

    var lambda = ActuallyInnerAlsoCompile();
    
    var dynamicAssembly = AppDomain.CurrentDomain.DefineDynamicAssembly(
        new AssemblyName("dynamicAssembly"),
        AssemblyBuilderAccess.Save);
    
    var dm = dynamicAssembly.DefineDynamicModule("dynamicModule", "dynamic.dll");
    var dt = dm.DefineType("dynamicType");
    var m1 = dt.DefineMethod(
        "dynamicMethod",
        MethodAttributes.Public | MethodAttributes.Static);
    
    lambda.CompileToMethod(m1);
    dt.CreateType();
    
    dynamicAssembly.Save("dynamic.dll");
    

    If you open that dynamic.dll with some IL tool (dotPeek for example) you will see something like:

    // Decompiled with JetBrains decompiler
    // Type: dynamicType
    // Assembly: dynamicAssembly, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null
    // MVID: 94346EDD-3BCD-4EB8-BA4E-C25343918535
    
    using System;
    using System.Collections.Generic;
    using System.Linq;
    
    internal class dynamicType
    {
      public static int dynamicMethod()
      {
        return ((IEnumerable<string>) new string[2]
        {
          "test",
          "test2"
        }).Where<string>(new Func<string, bool>(dynamicType.\u003CExpressionCompilerImplementationDetails\u003E\u007B1\u007Dlambda_method)).Select<string, int>(new Func<string, int>(dynamicType.\u003CExpressionCompilerImplementationDetails\u003E\u007B2\u007Dlambda_method)).FirstOrDefault<int>();
      }
    
      private static bool \u003CExpressionCompilerImplementationDetails\u003E\u007B1\u007Dlambda_method(string whereParam)
      {
        return whereParam.Length != 4;
      }
    
      private static int \u003CExpressionCompilerImplementationDetails\u003E\u007B2\u007Dlambda_method(string whereParam)
      {
        return whereParam.Length;
      }
    }
    

    Or (without ugly unicode escape sequences)