Skip to content

LightExpression no-argument constructor fast path misses returning rented lambda signature arrays #563

Closed

Description

@jogibear9988

The LightExpression root-lambda fast path for a class constructor with no arguments rents a parameter-signature array but does not return it to the existing pool when the lambda has unused parameters.

This is pre-existing: I verified the path in current master (f49acef0d83db3dd2b1059d7fda1a5107ee4eb3a) and in the source before #558 (e4c232da108d98790f0ca73f23ec90756da0cad7). It was not introduced by #558 or #559.

Reproduction

On modern .NET, using FastExpressionCompiler.LightExpression:

using System;
using FastExpressionCompiler.LightExpression;

public sealed class Item { }

public static class Program
{
    public static void Main()
    {
        var unused = Expression.Parameter(typeof(int), "unused");
        var tree = Expression.Lambda<Func<int, Item>>(
            Expression.New(typeof(Item)), unused);

        Func<int, Item> compiled = null;
        for (var i = 0; i < 1000; ++i)
            compiled = tree.CompileFast(ifFastFailedReturnNull: true);

        const int iterations = 10000;
        var start = GC.GetAllocatedBytesForCurrentThread();
        for (var i = 0; i < iterations; ++i)
            compiled = tree.CompileFast(ifFastFailedReturnNull: true);

        Console.WriteLine((double)(GC.GetAllocatedBytesForCurrentThread() - start) / iterations);
        GC.KeepAlive(compiled);
    }
}

The constructor itself has no arguments; the lambda accepts a parameter that its body does not use. A lambda without parameters uses the shared closure-only signature and is unaffected.

Cause

In the compiler source at current master, the root path calls RentPooledOrNewClosureTypeToParamTypes and then returns directly from CompileNoArgsNew. That helper frees its IL generator, but does not return the signature array. The nested-lambda caller does return the array after calling the helper, so the two callers currently have different ownership behavior.

This causes avoidable allocation on repeated compilation, rather than permanently retaining the arrays.

Measured allocation impact and local fix

A small local fix makes CompileNoArgsNew return the signature after delegate creation, and removes the nested caller's corresponding return to avoid returning the same array twice. It reuses the existing pool and changes neither emitted IL nor the context layout. This candidate is committed locally and has not yet been pushed.

Measured on Windows ARM64, with tiering and ReadyToRun disabled, two opposite-order process rounds, seven samples per round, 1,000 warmup compilations and 2,000 compilations per sample:

Runtime Unused lambda parameters Before, B/compile With local fix, B/compile Saved
.NET 8 1 944 904 40 B
.NET 8 2 960 912 48 B
.NET 10 1 944 904 40 B
.NET 10 2 960 912 48 B
.NET Framework 4.x (net472 target) 1 1232 1192 40 B
.NET Framework 4.x (net472 target) 2 1248 1200 48 B

Absolute totals depend on runtime and harness. The delta corresponds to the signature array including the hidden closure parameter. The zero-parameter constructor and arithmetic controls had unchanged allocation. Delegate invocation allocation was unchanged.

Compilation timings were mixed across runtimes, so this is an allocation improvement; I am not claiming a universal compilation speedup. The compared source snapshots were the local #559 state before this fix (201db040) and the local fix (fab2d6bd); the missing-return path was separately verified on master as noted above.

The local regression test, IntrinsicConstructorExceptionRegressionTests.No_argument_constructors_keep_alternating_unused_parameter_signatures, alternates integer, string, two-parameter and void-returning signatures, then invokes earlier delegates again to verify that returning the array does not corrupt their signatures. It covers default flags, disabled invocation inlining, and delegate debug information. The full native .NET 10 test run and focused .NET 8/.NET 10/.NET Framework probes passed.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions