So I came across this little gem in our codebase the other day, and I wanted to try and see if the person who wrote it was just lazy, or knew something that I don't.
A standard event handler was written like this (I will -
private void OnSomeEvent(IVehicle sender, ISomeArgs args){
if((sender is Car) && (sender as Car).numWheels == 4 && (sender as Car).hasGas)
{
(sender as Car).drive();
}
}
I saw this an immediately thought of the numerous un-boxing type-casting operations that are needlessly being done here. I re-wrote it like so-
private void OnSomeEvent(IVehicle sender, ISomeArgs args){
if (sender is Car){
Car _car = sender as Car;
if(_car.numWheels == 4 && _car.hasGas){
_car.drive();
}
}
}
So did the first example know something I don't? Does the compiler know that we are trying to un-box type-cast to the same type a bunch of times and do some optimization?
You've already accepted an answer, but I've already done the looking so I figure I'll post it.
Checking the output IL when doing a Release build, there's very little difference between the two setups.
The output IL from the first "unoptimized" call looks like so:
// Code size 47 (0x2f)
.maxstack 8
IL_0000: ldarg.0
IL_0001: isinst TestApp.Program/Car
IL_0006: brfalse.s IL_002e
IL_0008: ldarg.0
IL_0009: isinst TestApp.Program/Car
IL_000e: callvirt instance int32 TestApp.Program/Car::get_numWheels()
IL_0013: ldc.i4.4
IL_0014: bne.un.s IL_002e
IL_0016: ldarg.0
IL_0017: isinst TestApp.Program/Car
IL_001c: callvirt instance bool TestApp.Program/Car::get_hasGas()
IL_0021: brfalse.s IL_002e
IL_0023: ldarg.0
IL_0024: isinst TestApp.Program/Car
IL_0029: callvirt instance void TestApp.Program/Car::drive()
IL_002e: ret
The output IL from the second "optimized" call looks like:
// Code size 34 (0x22)
.maxstack 2
.locals init ([0] class TestApp.Program/Car c)
IL_0000: ldarg.0
IL_0001: isinst TestApp.Program/Car
IL_0006: stloc.0
IL_0007: ldloc.0
IL_0008: brfalse.s IL_0021
IL_000a: ldloc.0
IL_000b: callvirt instance int32 TestApp.Program/Car::get_numWheels()
IL_0010: ldc.i4.4
IL_0011: bne.un.s IL_0021
IL_0013: ldloc.0
IL_0014: callvirt instance bool TestApp.Program/Car::get_hasGas()
IL_0019: brfalse.s IL_0021
IL_001b: ldloc.0
IL_001c: callvirt instance void TestApp.Program/Car::drive()
IL_0021: ret
There are additional isinst
calls being made in the "unoptimized" call, which are good to be optimized away, but won't substantially impact the runtime of the function.
That said, I'd still do the "optimization" as the C# code is easier to read, which is more important than any performance micro-optimizations (until you profile your code and determine that you need to optimize a performance bottleneck).
(Also, the stack is slightly larger, but since it's just a stack increase which is fast and immediately cleaned up, and it's only 6 bytes, it's extremely minor.)