SYSK 116: Which Code is Faster and Why – Using ‘As’ or ‘Is’ + cast?
Which code segment below is faster?
#1
MyClass classInstance = new MyClass();
IDoWork iDoWorkClass = classInstance as IDoWork;
if (iDoWorkClass != null)
{
// Do something so compiler doesn't think it's dead code
System.Diagnostics.Debug.WriteLine("testing");
}
#2
MyClass classInstance = new MyClass();
if (classInstance is IDoWork)
{
IDoWork iDoWorkClassViaCast = (IDoWork)classInstance;
// Do same thing as above so we compare apples to apples
System.Diagnostics.Debug.WriteLine("testing");
}
If you answered ‘neither, they are approximately the same’ – you would be right based on my tests and analysis.
To understand why, look at the IL that’s produced (I added comments in italic to ease the reading of the IL code below):
IL for the first code snippet (using ‘as’):
// Code size 31 (0x1f)
// declare the maximal number of variables we plan to have on the stack at any given time
.maxstack 2
// declare variables and assign initial values to them
.locals init ([0] class AsIsComparison.MyClass classInstance,
[1] class AsIsComparison.IDoWork iDoWorkClass,
[2] bool CS$4$0000)
// NOP (no operation) instructions in the assembly code are needed to align labels to specific boundaries. This makes instruction-fetch operations more efficient for some processors
IL_0000: nop
// Create new object of type AsIsComparison.MyClass
IL_0001: newobj instance void AsIsComparison.MyClass::.ctor()
// store the current stack value in local variable 0, i.e. classInstance (see declaration above)
IL_0006: stloc.0
// load variable 0 onto stack
IL_0007: ldloc.0
// store the current stack value in local variable 1 -- iDoWorkClass
IL_0008: stloc.1
// load variable 1 onto stack
IL_0009: ldloc.1
// Push a null reference onto the evaluation stack
IL_000a: ldnull
// Compare two values. If they are equal, the integer value 1 (int32) is pushed onto the evaluation stack; otherwise 0 (int32) is pushed onto the evaluation stack.
IL_000b: ceq
// store the current stack value in local variable 2 -- (bool CS$4$0000, which is a compiler created variable)
IL_000d: stloc.2
// load variable 2 onto stack
IL_000e: ldloc.2
// Transfers control to a target instruction (at address IL_001e) if value is true, not null, or non-zero.
IL_000f: brtrue.s IL_001e
// Fill space if opcodes are patched. No meaningful operation is performed although a processing cycle can be consumed.
IL_0011: nop
// load the string constant onto the stack
IL_0012: ldstr "testing"
// Invoke method
IL_0017: call void [System]System.Diagnostics.Debug::WriteLine(string)
// No-op
IL_001c: nop
// No-op
IL_001d: nop
// Return
IL_001e: ret
IL for the second code snippet (using ‘is’ + cast):
// Code size 31 (0x1f)
// declare the maximal number of variables we plan to have on the stack at any given time
.maxstack 2
// declare variables and assign initial values to them
.locals init ([0] class AsIsComparison.MyClass classInstance,
[1] class AsIsComparison.IDoWork iDoWorkClassViaCast,
[2] bool CS$4$0000)
// NOP (no operation) instructions in the assembly code are needed to align labels to specific boundaries. This makes instruction-fetch operations more efficient for some processors
IL_0000: nop
// Create new object of type AsIsComparison.MyClass
IL_0001: newobj instance void AsIsComparison.MyClass::.ctor()
// store the current stack value in local variable 0, i.e. classInstance (see declaration above)
IL_0006: stloc.0
// load variable 0 onto stack
IL_0007: ldloc.0
// Push a null reference onto the evaluation stack
IL_0008: ldnull
// Compare two values. If they are equal, the integer value 1 (int32) is pushed onto the evaluation stack; otherwise 0 (int32) is pushed onto the evaluation stack.
IL_0009: ceq
// store the current stack value in local variable 2 -- (bool CS$4$0000, which is a compiler created variable)
IL_000b: stloc.2
// load variable 2 onto stack
IL_000c: ldloc.2
// Transfers control to a target instruction (at address IL_001e) if value is true, not null, or non-zero.
IL_000d: brtrue.s IL_001e
// Fill space if opcodes are patched. No meaningful operation is performed although a processing cycle can be consumed.
IL_000f: nop
// load variable 0 (classInstance) onto stack
IL_0010: ldloc.0
// store the current stack value in local variable 1 (iDoWorkClassViaCast)
IL_0011: stloc.1
// load the string constant onto the stack
IL_0012: ldstr "testing"
// Invoke method
IL_0017: call void [System]System.Diagnostics.Debug::WriteLine(string)
// No-op
IL_001c: nop
// No-op
IL_001d: nop
// Return
IL_001e: ret
The code size is the same… The number of operations is the same… The operations themselves are the same (the order varies in a couple of places)… That’s why the performance is the same!
Comments
- Anonymous
May 01, 2006
Have you tried to cast on a class rather than an interface. The IL does not make use of the castclass instruction in your example.
I previously noted another MS blogger stating that the as operation was faster than the cast operation. - Anonymous
May 01, 2006
My tests show that, by itself, the 'as' comparison is faster than 'is'. However, once you add the cast (to an interface or class) in the 'is' case and the comparison to null in the 'as' case, the performance is roughly the same. - Anonymous
May 01, 2006
You should note that there is no castclass or isclass opcode in the IL in both cases. That suggests that the compiler was able to optimize them away. This code creates an instance of the class MyClass and then uses is/as. The compiler knows that MyClass implements IDoWork and since classInstance variable is of type MyClass there is really no is/as to perform. The only thing that is still tested is a null reference since that will always return false/null for is/as operators.
If you really want to do is/as in action you need something like this:
interface ITest { }
class A { }
class B : A, ITest { }
TestIsAs(A a)
{
ITest t = a as ITest;
...
}
or even simpler:
TestIsAs(object o)
{
string s = o as string
...
}
I'd say that making the same cast or "is" should be avoided and relaying on the compiler to optimize some particular cases is not a good idea. FxCop even has a rule that detectes things like this :).