Search code examples
c#performancelambda

Are C# anonymous lambdas evaluated every time they are used?


Following How to decorate code in C# wiithout C/C++ - like macros I have just switched my logging system over to accept log messages as lambdas rather than as strings:

void Log(Func<string> msg)
{
    if (logIsEnabled)
    {
        Debug.Write(msg());
    }
}

My understanding is that this is much better, performance-wise, in that if I write:

Log(() => "foo(" + myInteger + ")");

then the string foo(42) is constructed only if logIsEnabled. For the sake of this question, let's assume that Log() is being called at high frequency, and constructing these strings for the log comes at an undesirable cost.

Suddenly I worried, though - is a lambda being instantiated every time this line of code is reached? Might that be more of a performance cost than constructing a string? I'm not clear on what's happening here, under the hood.

So, my question is: how is the lambda actually implemented? Is it constructed at compile-time and passed just as a pointer to a function? Is it constructed on first use and on subsequent passes just a pointer? Or is it constructed every time the line of code executes?


Solution

  • is a lambda being instantiated every time this line of code is reached?

    Yes. Not only a new Func<string> is instantiated every time, but also a small compiler-generated class. Let's post some code to SharpLab, and see what comes out:

    using System;
    
    class Program
    {
        static bool logIsEnabled;
    
        static void Main()
        {
            logIsEnabled = true;
            int myInteger = 13;
            Log(() => "foo(" + myInteger + ")");
        }
    
        static void Log(Func<string> msg)
        {
            if (logIsEnabled)
            {
                Console.WriteLine(msg());
            }
        }
    }
    

    SharpLab output (sanitized):

    using System;
    using System.Runtime.CompilerServices;
    
    internal class Program
    {
        [CompilerGenerated]
        private sealed class DisplayClass
        {
            public int myInteger;
    
            internal string M()
            {
                return string.Concat("foo(", myInteger.ToString(), ")");
            }
        }
    
        private static bool logIsEnabled;
    
        private static void Main()
        {
            DisplayClass displayClass = new DisplayClass();
            logIsEnabled = true;
            displayClass.myInteger = 13;
            Log(new Func<string>(displayClass.M));
        }
    
        private static void Log(Func<string> msg)
        {
            if (logIsEnabled)
            {
                Console.WriteLine(msg());
            }
        }
    }
    

    The DisplayClass is the closure that the compiler had to generate, in order to hold the myInteger variable. This variable is hoisted to a public field of the DisplayClass class.

    The Visual Studio can help you at detecting that a variable has been captured. Just hover the mouse over the lambda operator (=>).

    Visual Studio screenshot

    It is possible to avoid the allocation of the two objects by passing the myInteger as an argument, instead of relying on the convenience of captured variables and closures. Here is how:

    using System;
    
    class Program
    {
        static bool logIsEnabled;
    
        static void Main()
        {
            logIsEnabled = true;
            int myInteger = 13;
            Log(arg => "foo(" + arg + ")", myInteger);
        }
    
        static void Log<TArg>(Func<TArg, string> msg, TArg arg)
        {
            if (logIsEnabled)
            {
                Console.WriteLine(msg(arg));
            }
        }
    }
    

    SharpLab output (sanitized):

    using System;
    using System.Runtime.CompilerServices;
    
    internal class Program
    {
        [Serializable]
        [CompilerGenerated]
        private sealed class C
        {
            public static readonly C singleton = new C();
    
            public static Func<int, string> lambda;
    
            internal string M(int arg)
            {
                return string.Concat("foo(", arg.ToString(), ")");
            }
        }
    
        private static bool logIsEnabled;
    
        private static void Main()
        {
            logIsEnabled = true;
            int arg = 13;
            Log(C.lambda ?? (C.lambda = new Func<int, string>(C.singleton.M)), arg);
        }
    
        private static void Log<TArg>(Func<TArg, string> msg, TArg arg)
        {
            if (logIsEnabled)
            {
                Console.WriteLine(msg(arg));
            }
        }
    }
    

    Now the compiler generated a singleton (the C class), and the Func<TArg, string> is instantiated only once per TArg type. So if your program uses the Log<TArg> with ints, strings and decimals, only a Func<int, string>, a Func<string, string> and a Func<decimal, string> will be created in total, irrespective of how many times the Log<TArg> will be invoked.

    In case you want to pass more than one arguments to the Log method, you'll have to write additional Log<TArg1, TArg2>, Log<TArg1, TArg2, TArg3> etc overloads.