Search code examples
c#delegatesexpressionexpression-trees

Compiled lambda expression leading to new delegate allocations, whereas non-expression version does not


This compiled expression tree...

var param = Expression.Parameter(typeof(int));
var innerParam = Expression.Parameter(typeof(Action<int>));
var inner = Expression.Lambda(innerParam.Type, Expression.Block(), "test", new[] { param });
var outer = Expression.Lambda<Action<int>>(Expression.Block(new[] { innerParam }, Expression.Assign(innerParam, inner), Expression.Invoke(innerParam, param)), param).Compile();

Appears to lead to the creation of a new delegate on every single invocation of outer - observable if eg setting a breakpoint in Delegate.CreateDelegateNoSecurityCheck.

In contrast, the non-expression based equivalent of this function

Action<int> outer = x =>
{
  Action<int> innerParam = _ => { };
  innerParam(x);
};

does not appear to do this; repeated invocations of outer do not need to any new delegate allocations.

I struggle do understand why. Is this intended? Is there any nifty trick to have the expression-based version's delegates cached?


For context: This came up when using an external Deserialization library that seemed to allocate an unreasonable amount of memory through Delegate creation in our process. In effect, it does something very similar - it creates deserializers via Expression trees, and assigns delegates to local variables to support recursive & circular deserialization.


Solution

  • I struggle do understand why. Is this intended? Is there any nifty trick to have the expression-based version's delegates cached?

    It's because you don't explicitly cache the result of the inner delegate expression compilation.

    In the case with the non-expresion based solution the outerLambda is actually turned into a class with a field that can hold onto the innerLambda when the class is instantiated. Then calls to outer will be instance method invocations that make use of already created action delegate in the field:

    private sealed class <>c__DisplayClass0_0
    {
        public Action<int> inner;
    
        internal void <Main>b__1(int x)
        {
            Action<int> innerParam = inner;
            innerParam(x);
        }
    }
    

    Fortunately, if we make use of Expression.Constant we can have the same functionality with expressions.

    Looking at what Compile does in the source code - it creates a Delegate at the end by calling this method:

    private Delegate CreateDelegate() {
        Debug.Assert(_method is DynamicMethod);
    
        return _method.CreateDelegate(_lambda.Type, new Closure(_boundConstants.ToArray(), null));
    }
    

    This Closure instance will serve as the object Target of the Action<int> outer Delegate. That way the delegate can pass it as argument when invoking the actual compiled outer method:

    Void lambda_method205(System.Runtime.CompilerServices.Closure, Int32)
    

    If we have a look at Closure's source code we see that it has:

    /// <summary>
    /// Represents the non-trivial constants and locally executable expressions that are referenced by a dynamically generated method.
    /// </summary>
    public readonly object[] Constants;
    

    which is populated by VariableBinder.VisitConstant during the traversing stage before the actual compilation of the delegate:

    ...
    // Constants that can be emitted into IL don't need to be stored on
    // the delegate
    if (ILGen.CanEmitConstant(node.Value, node.Type)) {
        return node;
    }
    
    _constants.Peek().AddReference(node.Value!, node.Type);
    ...
    

    So to modify the original example (with testing code credits to the other answer by Guru Stron) we would have:

    static void Main() {
        var outerOriginal = CreateOuter(useConstantExpression: false);
    
        //outerOriginal.Target.Dump();
        MeasureAllocationsForInvocations(outerOriginal); // 8168 on my pc
        var outerWithConstantExpression = CreateOuter(useConstantExpression: true);
        // the Target of the delegate has the cached value of the inner Action<int>
        ////outerWithConstantExpression.Dump();
        //outerWithConstantExpression.Dump();
    
        MeasureAllocationsForInvocations(outerWithConstantExpression); // 0
    
        dynamic del = outerWithConstantExpression;
        // del.Target is Closure (using dynamic because I cannot reference the type even though it's public.. would edit if somebody assists)
        Console.WriteLine(del.Target.Constants[0].Method.ToString());
        // Void lambda_method204(System.Runtime.CompilerServices.Closure, Int32)
        Console.WriteLine(del.Method.ToString());
        //Void lambda_method205(System.Runtime.CompilerServices.Closure, Int32)
    }
    
    
    
    static void MeasureAllocationsForInvocations(Action<int> action) {
        var totalAllocatedBytes = GC.GetTotalAllocatedBytes();
        for (int i = 0; i < 100; i++) {
            action(1);
        }
        Console.WriteLine();
        Console.WriteLine(GC.GetTotalAllocatedBytes() - totalAllocatedBytes);
    }
    
    static Action<int> CreateOuter(bool useConstantExpression, bool useConsoleWriteLineInnerBody = false) {
    
        var intParamInnerAndOuter = Expression.Parameter(typeof(int));
    
        var innerBlock = Expression.Block();
        // for debugging
        if (useConsoleWriteLineInnerBody) {
            MethodInfo writeLineMethod = typeof(Console).GetMethod("Write",
                  new Type[] { typeof(int) });
            var writeLineCall = Expression.Call(writeLineMethod, intParamInnerAndOuter);
            innerBlock = Expression.Block(writeLineCall);
        }
    
        var innerParam = Expression.Parameter(typeof(Action<int>));
        var inner = Expression.Lambda(innerParam.Type,
            innerBlock, intParamInnerAndOuter);
    
        Expression expressionAssignment = useConstantExpression ?
            Expression.Constant(inner.Compile()) : inner;
    
        var outerBlock = Expression.Block(
           new[] { innerParam }, // Action<int> a;
         Expression.Assign(innerParam, expressionAssignment), 
         // a = closureParam.Constants[0]
         // or
         // a = new Action<int>(innerExpressionCompiledMethod)
         Expression.Invoke(innerParam, intParamInnerAndOuter)); // a(intParam);
    
        var outerExpression = Expression.Lambda<Action<int>>(outerBlock,
            intParamInnerAndOuter);
    
        var outer = outerExpression.Compile();
        // if constant it will inside do -> new Closure(new object[]{inner.Compile});
        return outer;
    }
    

    The only difference is in:

    Expression expressionAssignment = useConstantExpression ?
            Expression.Constant(inner.Compile()) : inner;
    

    This assignment to the local variable in the outer expression either uses the "cached" compiled delegate from closureParam.Constants[0] or will create each time new delegate based on the inner expression.

    In the testing code (for my pc) the memory allocated is 8168 bytes for the original case and 0 for the second that relies on Expression.Constant