Search code examples
c#.netcompiler-constructioncompiler-optimization

Understanding Compiler Optimizations


I'm trying to understand what the compiler is doing to very simple piece of code:

if (group.ImageHeight > 1 && group.ImageWidth > 1)
{ //No code exists between the braces
}

After compiling in Debug configuration, then decompiling I see this:

if (group.ImageHeight <= 1 || group.ImageWidth <= 1);

Decompiling a Release configuration results in

if (group.ImageHeight > 1)
{
  int imageWidth = group.ImageWidth;
}

More complete (original) code:

public class Group 
{
  public int ImageHeight { get; set; }
  public int ImageWidth { get; set; }
}

//The following belongs to a different project than `Group`
static void Main(string[] args)
{
  Group group = new Group();
  MyMethod(group);
}
static void MyMethod(Group group)
{
    if (group.ImageHeight > 1 && group.ImageWidth > 1)
    { 
    }
}

Here are my guesses and observations so far:

  • When I first started this I expected the compiler to completely drop the entire statement. I think that it's not because the evaluation of the properties could have side effects.
  • I believe that it's important that group type belongs to another project in my solution. I say this because the compiler probably can't "know" what the side effects of evaluating the properties could be in the future. For instance, I could, after compiling, replace the DLL that contains the definition for group.
  • In the Release config the possible side effects appear to be the same as my code: ImageHeight is evaluated and if meets the > 1 condition will evaluate ImageWidth (although through assignment rather than comparison)

Now, for my specific questions:

  • Why does the Release config use an assignment (int imageWidth = group.ImageWidth) rather than my original comparison? Is it faster to run an assignment?
  • Why does the Debug configuration completely change the possibility of side effects? In this configuration both ImageHeight and ImageWidth will always be evaluated.

Solution

  • For the first specific question. When you look at IL on sharplab.io The simple assignment is 1 compare instruction short. Whose "then" and "else" would point to the same instruction (in this case IL_0012) so compare there is not needed for calling function and two pops are enough. Weird is only loading the Int32 constant 1 which will be discarded immidiately.

    if (group.ImageHeight > 1)

    IL_0000: ldarg.0
    IL_0001: callvirt instance int32 Group::get_ImageHeight()
    IL_0006: ldc.i4.1
    IL_0007: ble.s IL_0012
    

    int imageWidth = group.ImageWidth;

    IL_0009: ldarg.0
    IL_000a: callvirt instance int32 Group::get_ImageWidth()
    IL_000f: ldc.i4.1
    IL_0010: pop
    IL_0011: pop
    
    IL_0012: ret
    

    For second specific question. If you look at IL on the same page with Debug mode, you'll see, that the code is identical only with some additional instructions for debuging and the compare itself so you can watch the result of it in debuger.

    IL_0000: nop
    IL_0001: ldarg.0
    IL_0002: callvirt instance int32 Group::get_ImageHeight()
    IL_0007: ldc.i4.1
    IL_0008: ble.s IL_0015
    
    IL_000a: ldarg.0
    IL_000b: callvirt instance int32 Group::get_ImageWidth()
    IL_0010: ldc.i4.1
    IL_0011: cgt
    IL_0013: br.s IL_0016
    
    IL_0015: ldc.i4.0
    
    IL_0016: stloc.0
            // sequence point: hidden
    IL_0017: ldloc.0
    IL_0018: brfalse.s IL_001c
    
    IL_001a: nop
    IL_001b: nop
    
    IL_001c: ret