Sunday 18 November 2007

Comparing ISINST to CASTCLASS

I was reading through a corporate C# coding standard the other day for one of my clients, and noticed that one of the coding requirements in the standard related to casting of types in code.
Their standard stated that all type conversions in C# should be accomplished by using the 'as' keyword rather than using a direct cast (i.e. '(type)').
I quizzed my client on this to see what formed the basis of their choice to use 'as'. They had no explanation, but said that they felt it could be faster.

Following on from this, I was interested to see whether using 'as' or '(type)' was faster for type conversion.

In .Net IL, 'as' is compiled to 'isinst'. isinst performs a type conversion and returns null if the specified type conversion is not valid.
This differs from using '(type)', which compiles to 'castclass'. castclass also performs a type conversion but throws an Exception if the specified type conversion is not valid.
This is an important difference. For example, consider the following example using 'as' (isinst):
void Foo1(object ObjA)
{
  ClassDef ObjB;

  ObjB = ObjA as ClassDef;
  ObjB.Property = "12345";
}


In the example, we have to be certain that we know ObjA can be converted to the ClassDef type, ObjB, because if it cannot, a NullReferenceException will occur in the line where the property is set.
A better implementation of the above example could be:
void Foo2(object ObjA)
{
  ClassDef ObjB;

  ObjB = ObjA as ClassDef;
  if (ObjB != null)
    ObjB.Property = "12345";
}


If we consider the same example using '(type)' (castclass):
void Foo3(object ObjA)
{
  ClassDef ObjB;

  ObjB = (ClassDef)ObjA;
  ObjB.Property = "12345";
}


Again in this example we have to be sure that the conversion between the two types is valid, because else a TypeConversionException will occur on the line where ObjA is cast to ClassDef.
There could be two potential alternatives to the castclass example:
void Foo4(object ObjA)
{
  try
  {
    ClassDef ObjB;

    ObjB = (ClassDef)ObjA;
    ObjB.Property = "12345";
  }
  catch (Exception ex)
  {
    // Do something useful
    throw ex;
  }
}

void Foo5(object ObjA)
{
  ClassDef ObjB;

  if (ObjA is ClassDef)
  {
    ObjB = (ClassDef)ObjA;
    ObjB.Property = "12345";
  }
}


In the examples above, Foo1 and Foo3 would be appropriate to use if we knew that the object ObjA can definitely be converted to ClassDef. However, this may not always be likely, particularly in these examples becuase the parameter ObjA is an object type.
In these circumstances, it could be more appropriate to use Foo2, Foo4 or Foo5, because they offer security against failures during the conversion process.
In Foo2, we rely on isinst returning null if the conversion is not valid, and so we check for null after the conversion.
In Foo4, we handle an exception that could be raised during a failed castclass.
In Foo5, we first check to see whether ObjA is a ClassDef (using isinst - 'ObjA is ClassDef') and then perform the conversion in the knowledge that it will not cause an exception.

Now I had five potential examples for performing a type convesion, I thought it would be interesting to see whether there was a great deal of difference in the time taken to carry out the different examples.
I wrote a test project which used a high-precision StopWatch to time each of the procedures.
I timed each procedure as it performed one billion casts, repeated over 50 iterations to ensure that a good average time could be ascertained.
Following on from this I analysed the results to see which of the above five examples was fastest.

The total time for all 50 billion conversions for each test were:
Foo1 = 1,466,176 ms
Foo2 = 1,506,633 ms
Foo3 = 1,499,661 ms
Foo4 = 1,519,072 ms
Foo5 = 1,689,141 ms

And the average times per iteration (1 billion conversions) were:
Foo1 = 29,323.52 ms
Foo2 = 30,132.66 ms
Foo3 = 29,993.22 ms
Foo4 = 30,381.44 ms
Foo5 = 33,782.82 ms

Foo1 was the fastest, closely followed by Foo3 (which was around 2.284% slower).
However, as neither Foo1 or Foo3 contain any checks to ensure that the conversion was a success so they may be unsuitable for general use.

Of the remaining methods, Foo2 was fastest, which involved checking for a null value after using isinst, followed by Foo4 (the the try...catch block around castclass) and finally Foo5, which was a combination of isinst and castclass. Foo5 was significantly slower than all the other methods, being around 3.5 seconds\million conversions slower than the slowest of the other four methods.
In order to directly compare isinst and castclass, we can examine Foo1 and Foo3. Consider the IL for Foo1 and Foo3 respectively:
Foo1
...
IL_0014  ldloc.2
IL_0015  isinst    TestCastStyle.ClassDef
IL_001a  stloc.1
IL_001b  ldloc.1
IL_001c  ldstr     "12345"
IL_0021  callvirt  instance void TestCastStyle.ClassDef::set_Property(string)
...


Foo3
...
IL_0014  ldloc.2
IL_0015  castclass TestCastStyle.ClassDef
IL_001a  stloc.1
IL_001b  ldloc.1
IL_001c  ldstr     "12345"
IL_0021  callvirt  instance void TestCastStyle.ClassDef::set_Property(string)
...


The code for the methods is identical, other than IL_0015, which uses isinst in Foo1 and castclass in Foo3.

In the tests I ran, Foo1 took around 29.32ms\million conversions whereas Foo3 took 29.99ms\million, which is a difference of 0.67ms\million.
This confirms that using isinst is definitely quicker than castclass.

Considering the scenario where a check must be performed to ensure the conversion is valid, we can compare Foo2 (isinst) to Foo4 or Foo5 (castclass). The IL for these methods is:
Foo2
...
IL_0014  ldloc.2
IL_0015  isinst    TestCastStyle.ClassDef
IL_001a  stloc.1
IL_001b  ldloc.1
IL_001c  brfalse.s IL_0029
IL_001e  ldloc.1
IL_001f  ldstr     "12345"
IL_0024  callvirt  instance void TestCastStyle.ClassDef::set_Property(string)
...


Foo4
...
try
{
  IL_0014  ldloc.2
  IL_0015  castclass TestCastStyle.ClassDef
  IL_001a  stloc.1
  IL_001b  ldloc.1
  IL_001c  ldstr     "12345"
  IL_0021  callvirt  instance void TestCastStyle.ClassDef::set_Property(string)
  IL_0026  leave.s   IL_002b
}
catch [mscorlib]System.Exception
{
  IL_0028  pop
  IL_0029  rethrow
}
...


Foo5
...
IL_0014  ldloc.2
IL_0015  isinst    TestCastStyle.ClassDef
IL_001a  brfalse.s IL_002e
IL_001c  ldloc.2
IL_001d  castclass TestCastStyle.ClassDef
IL_0022  stloc.1
IL_0023  ldloc.1
IL_0024  ldstr     "12345"
IL_0029  callvirt  instance void TestCastStyle.ClassDef::set_Property(string)
...


At first glance, I had assumed that Foo4 would be slowest of these methods, but the results were:
  • Foo2 took 30.13ms\million conversions

  • Foo4 took 30.38ms\million conversions

  • Foo5 took 33.78ms\million conversions


I was surprised to see that Foo5 was by far the slowest. There does not seem to be a massive difference between Foo2 and Foo4 under the test conditions, but if an Exception was raised at IL_0015 in Foo4, this would slow the method down considerably, whereas Foo2 would continue to be very quick.

The conclusions from the tests I performed are:
  • In a direct comparison, isinst is quicker than castclass (although on slightly)

  • When having to perform checks to ensure the conversion was successful, isinst was significantly quicker than castclass

  • A combination of isinst and castclass should not be used as this was far slower than the quickest "safe" conversion (over 12% slower)


If you would like to run the tests yourself, drop me an e-mail and I will send you the test program I used. Additionally, if you would like the analysis files containing the timings and code dumps, I can also send you this.

Have fun!

No comments: