Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Does the compiler optimizes repeated identical checks

I've come across some code that repeatedly checks the same condition. Seems like C# 6 will absolve us from this ugly redundant code, but in the meanwhile, is there any benefit to introducing the bool variable, or is the compiler smart enough to sort this out for us and not repeatedly compare the same things over and over? (even though we're doing a check anyways, I'll assume stashing the result in a bool would be (marginally) faster?)

// here we're doing the same check over and over again
string str1 = (CustomerData == null) ? string.Empty : CustomerData.str1;
string str2 = (CustomerData == null) ? string.Empty : CustomerData.str2;
string str3 = (CustomerData == null) ? string.Empty : CustomerData.str3;
// ... rinse and repeat

// here we're still doing a check, but against a boolean variable
bool is_valid  = CustomerData == null;
string str1 = is_valid ? string.Empty : CustomerData.str1;
string str2 = is_valid ? string.Empty : CustomerData.str2;
string str3 = is_valid ? string.Empty : CustomerData.str3;
// ... rinse and repeat

In this case this might not be critical, but what happens if wer'e comparing 2 objects which then need to go and deep check all the fields inside them?

Note: since this is inside a method, I couldn't rely on the default value for strings (null), so the workaround is creating all the strings, initializing them to string.Empty, and then doing something like:

if (CustomerData != null) {
    // set all of the above strings again, changing from empty to actual values  
}
like image 632
Noctis Avatar asked May 10 '26 04:05

Noctis


2 Answers

To expand on codenheim's answer.. it appears that, in Release builds, the JITter is smart enough to optimize them away.

The Debug build does all comparisons and jumps around a lot. A Release build (on x64 anyway..) produces:

; string str1 = (CustomerData == null) ? string.Empty : CustomerData.str1;
call        000000005F64D620 
mov         rdx,0E7A80733A0h 
mov         rdx,qword ptr [rdx] 
lea         rdi,[rbp+10h] 
mov         rcx,rdi 
call        000000005F64D620 
mov         rdx,0E7A80733A8h 
mov         rdx,qword ptr [rdx] 
lea         rbx,[rbp+18h] 
mov         rcx,rbx 
call        000000005F64D620 
mov         rsi,qword ptr [rsi] 
; string str2 = (CustomerData == null) ? string.Empty : CustomerData.str2;
mov         rdi,qword ptr [rdi] 
; string str3 = (CustomerData == null) ? string.Empty : CustomerData.str3;
mov         rbx,qword ptr [rbx] 
; string str6 = is_valid ? string.Empty : CustomerData.str3;
mov         rbp,qword ptr [rbp+18h] 

It seems that it just ignores your code and goes and moves the data to where it knows it should be.. given the result of an identical expression that was evaluated earlier on is known at that point in time.

like image 135
Simon Whitehead Avatar answered May 11 '26 18:05

Simon Whitehead


I suppose we have to be specific about which compiler. There are two compilers under consideration, the C# (source -> MSIL) and the JITter (MSIL -> native)

No, the Microsoft C# compiler does not rewrite this code to optimize away the multiple checks. In my experience, the C# compiler does very little optimization (for a reason) and MSIL amounts to intermediate code in the traditional compiler chain.

The C# code...

Customer CustomerData = new Customer();

string str1 = (CustomerData == null) ? string.Empty : CustomerData.str1;
string str2 = (CustomerData == null) ? string.Empty : CustomerData.str2;
string str3 = (CustomerData == null) ? string.Empty : CustomerData.str3;

Compiles in Release mode to MSIL

IL_0006:  ldloc.0               // load CustomerData
IL_0007:  brfalse.s  IL_0012    // if(CustomerData == ) ...

IL_0009:  ldloc.0               // load CustomerData
IL_000a:  ldfld      string ConsoleApplication1.Customer::str1
IL_000f:  pop
IL_0010:  br.s       IL_0018

IL_0012:  ldsfld     string [mscorlib]System.String::Empty
IL_0017:  pop
IL_0018:  ldloc.0               // load CustomerData
IL_0019:  brfalse.s  IL_0024    // if(CustomerData == null) ...

IL_001b:  ldloc.0               // load CustomerData
IL_001c:  ldfld      string ConsoleApplication1.Customer::str2
IL_0021:  pop
IL_0022:  br.s       IL_002a

IL_0024:  ldsfld     string [mscorlib]System.String::Empty
IL_0029:  pop
IL_002a:  ldloc.0               // load CustomerData
IL_002b:  brfalse.s  IL_0036    // if(CustomerData == null) ...

IL_002d:  ldloc.0               // load CustomerData
IL_002e:  ldfld      string ConsoleApplication1.Customer::str3
IL_0033:  pop
IL_0034:  br.s       IL_003c

As to whether the temporary variable works better, it comes down to which is faster operation:

ldloc

or

ldsfld

The local is faster once, but if the JITter happens to stash either one of them in a register, it won't make a difference.

Keep in mind, the MSIL is good to learn what is going on, but doesn't mean the JITter won't do more optimizations (I think we can assume it actually does do more) so to see that we'd need to dump the x86 code..

See Part 2 - SimonWhitehead (+1) dumped the x86/64 native result and we find that the JITter is more than just a pretty name for a translation engine - https://stackoverflow.com/a/26600198/257090

For what it's worth, I wouldn't lose sleep over it either way, the performance overhead is negligible (2 opcodes per field), just keep the conditions as-is, it makes the code cleaner.

like image 43
codenheim Avatar answered May 11 '26 16:05

codenheim