Search code examples
c#cilreflector

C# compiling optimizations: null coalescing operator - UPDATED - Reflector's bug?


Greetings! I am slightly confused about how does C# compiler perform its optimizations.
I have written the following getter to make up "lazy" initialization, and default value in case of null:

Static class Helper:

private static string host;  
public static string Host  
{        
    get  
    {  
        return host ?? (host= (ConfigurationManager.AppSettings["Host"] ?? "host.ru"));  
    }  
}

Here is the result of disassembling by Reflector:

public static string Host 
{  
    get  
    {  
        if (Helper.host == null)  
        {  
            string host = Helper.host;  
        }  
        return (Helper.host = ConfigurationManager.AppSettings["Host"] ?? "host.ru");  
    }  
}

Looks like it would work in other way than assumed...

UPDATE

    private static string host;
    public static string Host
    {
        get
        {
            return host ?? (host = (GetVal() ?? "default"));
        }
    }
    static void Main(string[] args)
    {

        Console.WriteLine(Host);
        host = "overwritten";
        Console.WriteLine(Host);
    }
    static string GetVal()
    {
        return "From config";
    }

Works correctly (From config, overwritten), but Reflector shows the same:

public static string Host
{
    get
    {
        if (Program.host == null)
        {
            string host = Program.host;
        }
        return (Program.host = GetVal() ?? "default");
    }
}

Solution

  • This looks like a bug in Reflector's C# disassembly.

    Starting with this code:

    public static string _test;
    public static string _setting;
    
    public static string Test_1
    {
        get { return _test ?? (_setting ?? "default"); }
    }
    

    Reflector shows this C# disassembly:

    public static string Test_1
    {
        get
        {
            return (_test ?? (_setting ?? "default"));
        }
    }
    

    and the corresponding IL:

    .method public hidebysig specialname static string get_Test_1() cil managed
    {
        .maxstack 8
        L_0000: ldsfld string ConsoleApplication1.Program::_test
        L_0005: dup 
        L_0006: brtrue.s L_0017
        L_0008: pop 
        L_0009: ldsfld string ConsoleApplication1.Program::_setting
        L_000e: dup 
        L_000f: brtrue.s L_0017
        L_0011: pop 
        L_0012: ldstr "default"
        L_0017: ret 
    }
    

    I am not an IL expert, but this is my take on it:

    • L_0000:ldsfld pushes _test onto the evaluation stack
    • L_0005:dup copies the value (_test) that is topmost on the evaluation stack and pushes that onto the stack.
    • L_0006:brtrue.s pops the value created by dup off the stack and jumps to L_0017 if it is not null.
    • L_0008:pop at this point, _test is null, so pop that value off the stack.

    and it continues to evaluate _setting in a similar fashion, finally returning "default" if _setting is also null.

    Now, if we add an assignment into the code like this:

    public static string Test_2
    {
        get { return _test ?? (_test = (_setting ?? "default")); }
    }
    

    Reflector shows this C# disassembly:

    public static string Test_2
    {
        get
        {
            if (_test == null)
            {
                string text1 = _test;
            }
            return (_test = _setting ?? "default");
        }
    }
    

    which is not correct (if _test is not null, instead of returning _test, it assigns _setting or "default" to _test and then returns).

    However, the IL dissassembly looks like the IL for Test_1, with a couple of extra instructions at L_0017 and L_0018 to do the assignment.

    .method public hidebysig specialname static string get_Test_2() cil managed
    {
        .maxstack 8
        L_0000: ldsfld string ConsoleApplication1.Program::_test
        L_0005: dup 
        L_0006: brtrue.s L_001d
        L_0008: pop 
        L_0009: ldsfld string ConsoleApplication1.Program::_setting
        L_000e: dup 
        L_000f: brtrue.s L_0017
        L_0011: pop 
        L_0012: ldstr "default"
        L_0017: dup 
        L_0018: stsfld string ConsoleApplication1.Program::_test
        L_001d: ret 
    }
    

    Finally, if you copy Reflector's C# dissembly and run it against the original, you'll see it produces different results.

    using System;
    
    namespace ConsoleApplication1
    {
        class Program
        {
            static void Main(string[] args)
            {
                _test = "Test";
                Console.WriteLine(Test_2);
                Console.WriteLine(Reflector_Test_2);
                Console.ReadLine();
            }
    
            public static string _test;
            public static string _setting;
    
            public static string Test_1
            {
                get { return _test ?? (_setting ?? "default"); }
            }
    
            public static string Test_2
            {
                get { return _test ?? (_test = (_setting ?? "default")); }
            }
    
            public static string Reflector_Test_2
            {
                get
                {
                    if (_test == null)
                    {
                        string text1 = _test;
                    }
                    return (_test = _setting ?? "default");
                }
            }
        }
    }
    

    Outputs

    Test
    default