3

I would like to understand the IL code a bit.

So as you see the expression bodied has less code than the GetOld code. Is it some optimization done there and means expression body syntax is more performant?

Or it does not really matter?

namespace DatabaseModules {
    public class Test {
        public IList<string> _cache = new List<string>();

        public Test() {

        }

        public IList<string> Get => _cache;

        public IList<string> GetOld {
            get { return _cache; }
        }
    }
}

And the generated IL Code using DotPeek

https://gist.github.com/anonymous/9673389a1a21d0ad8122ec97178cfd9a

SomeGuyWhoCodes
  • 579
  • 6
  • 19

2 Answers2

6

When compiling in Release mode, both properties produce the same IL:

.method public hidebysig specialname 
    instance class [mscorlib]System.Collections.Generic.IList`1<string> get_Get () cil managed 
{
    // Method begins at RVA 0x2063
    // Code size 7 (0x7)
    .maxstack 8

    IL_0000: ldarg.0
    IL_0001: ldfld class [mscorlib]System.Collections.Generic.IList`1<string> DatabaseModules.Test::_cache
    IL_0006: ret
} // end of method Test::get_Get

.method public hidebysig specialname 
    instance class [mscorlib]System.Collections.Generic.IList`1<string> get_GetOld () cil managed 
{
    // Method begins at RVA 0x2063
    // Code size 7 (0x7)
    .maxstack 8

    IL_0000: ldarg.0
    IL_0001: ldfld class [mscorlib]System.Collections.Generic.IList`1<string> DatabaseModules.Test::_cache
    IL_0006: ret
} // end of method Test::get_GetOld

When compiling in Debug mode, the IL is indeed different:

.method public hidebysig specialname 
    instance class [mscorlib]System.Collections.Generic.IList`1<string> get_Get () cil managed 
{
    // Method begins at RVA 0x2065
    // Code size 7 (0x7)
    .maxstack 8

    IL_0000: ldarg.0
    IL_0001: ldfld class [mscorlib]System.Collections.Generic.IList`1<string> DatabaseModules.Test::_cache
    IL_0006: ret
} // end of method Test::get_Get

.method public hidebysig specialname 
    instance class [mscorlib]System.Collections.Generic.IList`1<string> get_GetOld () cil managed 
{
    // Method begins at RVA 0x2070
    // Code size 12 (0xc)
    .maxstack 1
    .locals init (
        [0] class [mscorlib]System.Collections.Generic.IList`1<string>
    )

    IL_0000: nop
    IL_0001: ldarg.0
    IL_0002: ldfld class [mscorlib]System.Collections.Generic.IList`1<string> DatabaseModules.Test::_cache
    IL_0007: stloc.0
    IL_0008: br.s IL_000a
    IL_000a: ldloc.0
    IL_000b: ret
} // end of method Test::get_GetOld

The added instructions are:

  1. A nop instruction, whose purpose is to do nothing.
  2. A br instruction, which jumps to the following instruction; in effect, this does nothing.
  3. A stloc-ldloc pair of instructions, which store and then load the value from a local variable, which in effect does nothing.

So, the added instructions don't do anything. I believe most of them are there to aid in debugging (i.e. the nop is there so that you can place a breakpoint on the opening brace; this is represented by a comment in your dotPeek output). Some of them might be artifacts of how the compiler works internally and are not removed in Debug mode, because there is no reason to do that.

In the end, the difference doesn't matter. And it can't cause performance difference in Release mode, since there is no difference in IL in that mode.

svick
  • 236,525
  • 50
  • 385
  • 514
  • [Br_S](https://learn.microsoft.com/dotnet/api/system.reflection.emit.opcodes.br_s) & [Ldloc_0](https://learn.microsoft.com/en-us/dotnet/api/system.reflection.emit.opcodes.ldloc_0) : these additional instructions in debug build are to allow stepping on method having a `}` at the end as well as spying the returned result like you can verify using the VS debugger. With EB there is no needed step nor spy, so there is no additional code. –  Nov 07 '19 at 15:01
5

There is none. That code compiles to the exact same C# using Roslyn, so the IL is no different:

using System.Runtime.CompilerServices;
using System.Security;
using System.Security.Permissions;

[assembly: AssemblyVersion("0.0.0.0")]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.Default | DebuggableAttribute.DebuggingModes.DisableOptimizations | DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints | DebuggableAttribute.DebuggingModes.EnableEditAndContinue)]
[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)]
[module: UnverifiableCode]
namespace DatabaseModules
{
    public class Test
    {
        public IList<string> _cache = new List<string>();

        public IList<string> Get
        {
            get
            {
                return this._cache;
            }
        }

        public IList<string> GetOld
        {
            get
            {
                return this._cache;
            }
        }
    }
}
Patrick Hofman
  • 153,850
  • 22
  • 249
  • 325
  • Hmm what is this conversion actually doing? I did not really understand – SomeGuyWhoCodes Oct 02 '17 at 09:32
  • That is your code evaluated by the Roslyn compiler. Expression bodied members are no different than 'old' getters. – Patrick Hofman Oct 02 '17 at 09:33
  • Is that the flow always happens whenever we compile , that class gets generated by Roslyn and then gets compiled to IL? – SomeGuyWhoCodes Oct 02 '17 at 09:35
  • 1
    Roslyn parses your code and makes IL from it. As an intermediate step it creates expressions you can evaluate back, like the code above. – Patrick Hofman Oct 02 '17 at 09:36
  • 1
    @Sherry - during compilation, a fair few features in C# are "lowered" - turned into simpler/older style code. This particular feature could be implemented in the C# compiler without requiring any changes to the CLR, since it can be implemented in this fashion. Even e.g. `foreach` doesn't exist by the time the compiler is translating into IL - it's another feature that gets lowered to different code. – Damien_The_Unbeliever Oct 02 '17 at 09:59
  • [When I switch SharpLab to IL mode](https://sharplab.io/#v2:C4LglgNgNAJiDUAfAAgBgATIIwBYDcAsAFDEB2AhgLYCmAzgA7kDG16AIucOQEbm3UBZAPYwArhDroA3sXRzMAZkwAmdABU6wabPm7kSgJIAZMLWAAebKgB86APpNmAC1YBedKWoB3dCbOWsGwAKAEpCEiJdPSUNM1DtCKj5AF9iHST9dGNTCytbAHFqLVdbB2dqcPSozOz/PPRC4AB5CBgEpKSAcyLpTAB2e0cmFzx0VMiO8d1x5KA=), I see the difference. Just because decompiled C# is the same doesn't mean the underlying IL is also the same. – svick Oct 02 '17 at 17:08
  • @svick I understand. However, to my understanding those two blocks compile to the same expressions in Roslyn and thus the end result. I wonder what causes the difference in IL then. – Patrick Hofman Oct 02 '17 at 17:12
  • "As an intermediate step it creates expressions you can evaluate back, like the code above." That's not how decompilers (like dotPeek or ILSpy, which is used by SharpLab) work. Instead, they look at the generated IL and try to reconstruct the original C# code from it. – svick Oct 02 '17 at 17:27