1

I was going through my project to refactor some code and realized I had written something like this:

if(errorCode > 0)
{
   DisplayError(errorCode);
   return;
}

// Continue to do stuff otherwise.

As you may have guessed, the function has a return type of void. As I started to look this over, I pondered whether there was any real difference between putting this inside an if/else block instead:

if(errorCode > 0)
{
   DisplayError(errorCode);
}
else
{
   // Do other stuff
}

The else block will continue until the end of the function, so the flow of control is essentially the same. Is there a performance difference here, or a convention that should be used, or are these two really exactly the same?

AdamMc331
  • 16,492
  • 10
  • 71
  • 133
  • 4
    One more important thing to note, `return` will execute, irrespective of the result of `if` statement. It is not part of `if` block. – Habib Nov 19 '14 at 18:19
  • 3
    Attempting this kind of micro-optimization is almost always misguided - prefer readable code over assuming you're smarter than a compiler. If you have a case where you *must* squeeze performance, measure and benchmark, there is no silver bullet. – Preston Guillot Nov 19 '14 at 18:22
  • @PrestonGuillot mind if I ask your opinion on which is more readable? Imo, I would pick the if/else block, but I'm curious as to what you think. – AdamMc331 Nov 19 '14 at 18:23
  • 1
    The early return is typically more of a stylistic improvement than performance related, as it reduces nesting. See [here](http://stackoverflow.com/questions/268132/invert-if-statement-to-reduce-nesting) and [here](http://programmers.stackexchange.com/questions/18454/should-i-return-from-a-function-early-or-use-an-if-statement) – StuartLC Nov 19 '14 at 18:23
  • 3
    Given that your first snippet contains what appears to be a logic error, I'd say it's pretty clear that the second is more readable. – Preston Guillot Nov 19 '14 at 18:23
  • Made a typo. Edited question. – AdamMc331 Nov 19 '14 at 18:25
  • 1
    @Habib you're right. That was my typo, not an error in my project. I'm having no logic issues here, just a question of performance and/or convention. – AdamMc331 Nov 19 '14 at 18:26

2 Answers2

4

The generated code in both cases is completely identical.

(You are missing brackets around the code in the first example, but I will just assume that it's a typo and you are actually asking about the difference betwen using return and else.)

If you look at the generated code for these two methods:

public static void Test1(int errorCode) {
  if (errorCode > 0) {
    Console.WriteLine(errorCode);
    return;
  }
  Console.WriteLine("ok");
}

public static void Test2(int errorCode) {
  if (errorCode > 0) {
    Console.WriteLine(errorCode);
  } else {
    Console.WriteLine("ok");
  }
}

It will look like this:

            if (errorCode > 0) {
011A00DA  in          al,dx  
011A00DB  push        eax  
011A00DC  mov         dword ptr [ebp-4],ecx  
011A00DF  cmp         dword ptr ds:[10F3178h],0  
011A00E6  je          011A00ED  
011A00E8  call        7470C310  
011A00ED  cmp         dword ptr [ebp-4],0  
011A00F1  jle         011A0100  
                Console.WriteLine(errorCode);
011A00F3  mov         ecx,dword ptr [ebp-4]  
011A00F6  call        73C5A920  
                return;
011A00FB  nop  
011A00FC  mov         esp,ebp  
011A00FE  pop         ebp  
011A00FF  ret  
            }
            Console.WriteLine("ok");
011A0100  mov         ecx,dword ptr ds:[3E92190h]  
011A0106  call        7359023C  
        }
011A010B  nop  
011A010C  mov         esp,ebp  
011A010E  pop         ebp  
011A010F  ret

and:

            if (errorCode > 0) {
011A0122  in          al,dx  
011A0123  push        eax  
011A0124  mov         dword ptr [ebp-4],ecx  
011A0127  cmp         dword ptr ds:[10F3178h],0  
011A012E  je          011A0135  
011A0130  call        7470C310  
011A0135  cmp         dword ptr [ebp-4],0  
011A0139  jle         011A0148  
                Console.WriteLine(errorCode);
011A013B  mov         ecx,dword ptr [ebp-4]  
011A013E  call        73C5A920  
011A0143  nop  
011A0144  mov         esp,ebp  
011A0146  pop         ebp  
011A0147  ret  
            } else {
                Console.WriteLine("ok");
011A0148  mov         ecx,dword ptr ds:[3E92190h]  
011A014E  call        7359023C  
            }
        }
011A0153  nop  
011A0154  mov         esp,ebp  
011A0156  pop         ebp  
011A0157  ret

The generated code is completely identical, down to the last instruction.

Guffa
  • 687,336
  • 108
  • 737
  • 1,005
  • Wow, that was really helpful and really interesting at the same time. Mind if I ask how you go about generating code like that? I don't have any experience/exposure to it. – AdamMc331 Nov 19 '14 at 18:41
  • @McAdam331: In Visual Studio you can put a breakpoint on the code that you want to examine and debug. When the breakpoint hits you open the disassembly window (ctrl+alt+D). Note that code compiled in debug and release mode is different; in debug mode there may be extra `nop` instructions added for debugging purposes and the code is not optimised, in release mode the code can be rearranged by the optimisation so it might not exactly resemble the source code. – Guffa Nov 19 '14 at 19:09
0

Off topic, but too large for a comment, is that you can also use the excellent LinqPad for IL disassembly.

Interestingly, disassembling only as far as IL in debug mode, there are a couple of extra nop's in Guffa's Test2 IL, which get compiled out in the x86 / x64 JIT as per Guffa's answer:

Debug IL:

Test1:
IL_0000:  nop         
IL_0001:  ldarg.0     
IL_0002:  ldc.i4.0    
IL_0003:  cgt         
IL_0005:  ldc.i4.0    
IL_0006:  ceq         
IL_0008:  stloc.0     // CS$4$0000
IL_0009:  ldloc.0     // CS$4$0000
IL_000A:  brtrue.s    IL_0016
IL_000C:  nop         
IL_000D:  ldarg.0     
IL_000E:  call        System.Console.WriteLine
IL_0013:  nop         
IL_0014:  br.s        IL_0021
IL_0016:  ldstr       "ok"
IL_001B:  call        System.Console.WriteLine
IL_0020:  nop         
IL_0021:  ret         

Test2:
IL_0000:  nop         
IL_0001:  ldarg.0     
IL_0002:  ldc.i4.0    
IL_0003:  cgt         
IL_0005:  ldc.i4.0    
IL_0006:  ceq         
IL_0008:  stloc.0     // CS$4$0000
IL_0009:  ldloc.0     // CS$4$0000
IL_000A:  brtrue.s    IL_0017
IL_000C:  nop         
IL_000D:  ldarg.0     
IL_000E:  call        System.Console.WriteLine
IL_0013:  nop         
IL_0014:  nop     <-- Extra    
IL_0015:  br.s        IL_0024
IL_0017:  nop     <-- Extra
IL_0018:  ldstr       "ok"
IL_001D:  call        System.Console.WriteLine
IL_0022:  nop         
IL_0023:  nop     <-- Extra    
IL_0024:  ret         

This is interesting IMO since, from memory, processor nop's are wasteful if they actually get executed, but beneficial if they pad to allow a jump to reach a subsequent alignment boundary. Which begs the question as to why IL takes an opinion on nops at all, since the padding optimization should only be a JIT compiler concern. The answer seems unrelated, viz braces, probably for debugging reasons

For release mode, Guffa is correct - compiling the IL in release mode drops ALL nops from the IL - the IL nops are purely there for allowing debugging breaks / step throughs on the braces. The x86 nops are reintroduced by the Jitter for even instruction alignment, and the two methods are identical.

Release IL (Test1 and Test2):

IL_0000:  ldarg.0     
IL_0001:  ldc.i4.0    
IL_0002:  ble.s       IL_000B
IL_0004:  ldarg.0     
IL_0005:  call        System.Console.WriteLine
IL_000A:  ret         
IL_000B:  ldstr       "ok"
IL_0010:  call        System.Console.WriteLine
IL_0015:  ret  
Community
  • 1
  • 1
StuartLC
  • 104,537
  • 17
  • 209
  • 285
  • 1
    Did you compile in debug mode? There are extra nop instructions added when you do. – Guffa Nov 19 '14 at 22:45
  • Yup, that was it - enabling `optimize+` in LinqPad drops the nops + does other optimizations. IL becomes identical. – StuartLC Nov 20 '14 at 05:00