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
}
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With