Home > Enterprise >  What causes a memory allocation even when this Func<> variable is null?
What causes a memory allocation even when this Func<> variable is null?

Time:07-10

Using BenchmarkDotNet's memory diagnoser, this code seems to allocate 12B even when called with parameters = null:

public static void Usage(this ILogger logger, LogLevel logLevel, string area, string operation, Dictionary<string, string> parameters)
{
    Func<Dictionary<string, string>> function = null;
    if (parameters != null)
    {
        function = new Func<Dictionary<string, string>>(() =>
        {
            return parameters;
        });
    }
    logger.Usage(logLevel, area, operation, function);
}

If I remove the assignment to function, the allocation drops to 0. When I look at the IL code I can see the following lines:

IL_0000: newobj instance void MIT.Logging.Infrastructure.LoggerExtensions/'<>c__DisplayClass0_0'::.ctor()

IL_001f: newobj instance void class [mscorlib]System.Func`1<class [mscorlib]System.Collections.Generic.Dictionary`2<string, string>>::.ctor(object, native int)

The second line makes sense, it suppose to be conditional and in my specific case, the condition is NOT met. But the first one I can't explain.

This is the benchmark method:

[Benchmark]
public void Log_WithInfra_ExtensionMethodDirect_NoParameters()
{
    LoggerExtensions.Usage(_logger, LogLevel.Information, LogAreas.MainApplication.AreaName, LogAreas.MainApplication.Operations.Operation0, null);
}

This is the benchmark results:

Method Mean Error StdDev Gen 0 Allocated
Log_WithInfra_ExtensionMethodDirect_NoParameters 8.441 ns 18.732 ns 1.027 ns 0.0023 12 B

It is stupid, it is not that important to my use case, but it drives me crazy.

CodePudding user response:

The problem is that the scope of parameters extends beyond the scope of your conditional, so the closure that captures it is created at the top level.

Simplifying your original code to this, for example:

public static void Usage(string area, string operation, Dictionary<string, string> parameters)
{
    Func<Dictionary<string, string>> function = null;

    if (parameters != null)
    {
        function = new Func<Dictionary<string, string>>(() =>
        {
            return parameters;
        });
    }
}

... the C# 1.0 equivalent looks like this:

   <>c__DisplayClass5_0 <>c__DisplayClass5_ = new <>c__DisplayClass5_0 ();
   <>c__DisplayClass5_.parameters = parameters;
   Func<Dictionary<string, string>> function = null;
   if (<>c__DisplayClass5_.parameters != null)
   {
        function = new Func<Dictionary<string, string>> (<>c__DisplayClass5_.<Usage>b__0);
   }

You can see that the closure is created, and its property is set, so that it can be used if needed anywhere in the function. To avoid this happening when your condition is not met, create a separate variable scoped locally to your if condition.

public static void Usage(string area, string operation, Dictionary<string, string> parameters)
{
    Func<Dictionary<string, string>> function = null;

    if (parameters != null)
    {
        var capturable = parameters;
        function = new Func<Dictionary<string, string>>(() =>
        {
            return capturable;
        });
    }
}

That changes your C# 1.0-equivalent code to:

   Func<Dictionary<string, string>> function = null;
   if (parameters != null)
   {
        <>c__DisplayClass5_0 <>c__DisplayClass5_ = new <>c__DisplayClass5_0 ();
        <>c__DisplayClass5_.capturable = parameters;
        function = new Func<Dictionary<string, string>> (<>c__DisplayClass5_.<Usage>b__0);
   }

CodePudding user response:

I've tried this code:

    public static void Usage(this ILogger logger, LogLevel logLevel, string area, string operation, Dictionary<string, string> parameters)
    {
        Func<Dictionary<string, string>> function = null;

        if (parameters == null)
        {
            var capturable = parameters;
            function = () => capturable;
        }

        logger.Usage(logLevel, area, operation, function);
    }

And it results in 44B of allocation.

What am i missing?

Method Mean Error StdDev Gen 0 Allocated
Log_WithInfra_ExtensionMethodDirect_NoParameters 11.83 ns 4.657 ns 0.255 ns 0.0084 44 B
  • Related