Pages

Thursday, November 4, 2010

Rate your calls: Type Constraint Or Type Evaluation

You must be thinking why I am coming with such a strange Tag line today. Actually if you go on reading you would definitely understand what I am speaking on. In my last post, I have pointed out that Tuple is implemented in a strange way. If you see the 8th implementation of Tuple class, the one with 8 generic parameters, you will see, it gives a restriction to the last parameter TRest to allow only Tuple.

But the strange thing is, the restriction is not open. The Tuple class does not have this restriction specified in its generic type parameter TRest. So you can even declare the last argument as string, but when your application actually tries to create an object of it, the application will throw an ArgumentException.

This seemed to be very weird to me. I was thinking why did Microsoft did not put this in Type argument, making ITuple (an internal implementation) public, or create a special sealed class, and expose the same as a constraint to Type argument. I think this could be answered only by BCL Team.

Download Sample Code - 30KB



The Test

In this post, I will try to test the Two scenario :

  1. The performance between the two type of implementation, one with constraint given on Generic Type parameter directly and another with no constructor parameter but with a check in its constructor.
  2. The demonstration in terms of IL.

Well to do this, I have built two classes, and one Interface. I will try to give restriction to my class based on the Interface defined such that the second argument must be of that type. Now lets look at the implementation :

1. IMyType : A dummy interface.

public interface IMyType
{
    string Sample1 { get; set; }
}

2. MyType1<T1,T2>

public class MyType1<T1, T2> : IMyType
{
    public T1 Item1 { get; set; }
    public T2 Item2 { get; set; }

    public MyType1(T1 t1, T2 t2)
    {
        if(!(t2 is IMyType))
            throw new ArgumentException("Exception in argument list");

        this.Item1 = t1;
        this.Item2 = t2;
    }

    #region IMyType Members

    public string Sample1
    {
        get;
        set;
    }

    #endregion
}

So the class MyType1 actually puts the restriction from the constructor itself with a checking of its type.

3. MyType2<T1,T2>

public class MyType2<T1, T2> : IMyType where T2: IMyType 
{
    public T1 Item1 { get; set; }
    public T2 Item2 { get; set; }

    public MyType2(T1 t1, T2 t2)
    {
        this.Item1 = t1;
        this.Item2 = t2;
    }

    #region IMyType Members

    public string Sample1
    {
        get;
        set;
    }

    #endregion
}

In this implementation, the actual restriction is given from outside using Generic Type constraint.

Now lets run a test on both the classes to see which one performs well.
For simplicity purpose, I am just creating the objects of both the types and checking the time taken to create each of those objects:

Console.WriteLine("CurrentTime is : {0}", DateTime.Now.ToLongTimeString());
DateTime currentTime = DateTime.Now;
for (int i = 0; i < 20000000; i++)
{
    MyType1<string, MyDummyType> typ = new MyType1<string, MyDummyType>("thisObject", new MyDummyType());
    typ = null;
}
TimeSpan span = DateTime.Now.Subtract(currentTime);

Console.WriteLine("Created 2 crores of type1 : {0}, Time Elapsed : {1} ", DateTime.Now.ToLongTimeString(), span.Milliseconds);

currentTime = DateTime.Now;
for (int i = 0; i < 20000000; i++)
{
    MyType2<string, MyDummyType> typ = new MyType2<string, MyDummyType>("thisObject", new MyDummyType());
    typ = null;
}

span = DateTime.Now.Subtract(currentTime);

Console.WriteLine("Created 2 crores of type1 : {0}, Time Elapsed : {1} ", DateTime.Now.ToLongTimeString(), span.Milliseconds);

Console.ReadLine();

So when I run my code, it seems to that the one with Generic constraint runs half of time than the other one. So MyType2 perform better than MyType1.


And rightly so, as I thought. Hmm, actually if you look into the implementation of both of the classes, you can definitely see that MyType1 will have more IL expressions in its constructor than MyType2. When I create MyType1 for 2 crore times, it takes 843 milliseconds while the second takes only 359 milliseconds.  So it is almost less than half of the time on creating these types.

IL for the constructors: 

Now if you see the IL of both the constructor, it will look :

1. MyType1<T1,T2>

.method public hidebysig specialname rtspecialname 
        instance void  .ctor(!T1 t1,
                             !T2 t2) cil managed
{
  // Code size       55 (0x37)
  .maxstack  2
  .locals init ([0] bool CS$4$0000)
  IL_0000:  ldarg.0
  IL_0001:  call       instance void [mscorlib]System.Object::.ctor()
  IL_0006:  nop
  IL_0007:  nop
  IL_0008:  ldarg.2
  IL_0009:  box        !T2
  IL_000e:  isinst     StressTest.IMyType
  IL_0013:  ldnull
  IL_0014:  cgt.un
  IL_0016:  stloc.0
  IL_0017:  ldloc.0
  IL_0018:  brtrue.s   IL_0025
  IL_001a:  ldstr      "Exception in argument list"
  IL_001f:  newobj     instance void [mscorlib]System.ArgumentException::.ctor(string)
  IL_0024:  throw
  IL_0025:  ldarg.0
  IL_0026:  ldarg.1
  IL_0027:  call       instance void class StressTest.MyType1`2<!T1,!T2>::set_Item1(!0)
  IL_002c:  nop
  IL_002d:  ldarg.0
  IL_002e:  ldarg.2
  IL_002f:  call       instance void class StressTest.MyType1`2<!T1,!T2>::set_Item2(!1)
  IL_0034:  nop
  IL_0035:  nop
  IL_0036:  ret
}

2. MyType2<T1,T2>

.method public hidebysig specialname rtspecialname 
        instance void  .ctor(!T1 t1,
                             !T2 t2) cil managed
{
  // Code size       26 (0x1a)
  .maxstack  8
  IL_0000:  ldarg.0
  IL_0001:  call       instance void [mscorlib]System.Object::.ctor()
  IL_0006:  nop
  IL_0007:  nop
  IL_0008:  ldarg.0
  IL_0009:  ldarg.1
  IL_000a:  call       instance void class StressTest.MyType2`2<!T1,!T2>::set_Item1(!0)
  IL_000f:  nop
  IL_0010:  ldarg.0
  IL_0011:  ldarg.2
  IL_0012:  call       instance void class StressTest.MyType2`2<!T1,!T2>::set_Item2(!1)
  IL_0017:  nop
  IL_0018:  nop
  IL_0019:  ret
} 

So basically if you see the IL for both the cases, it seems the former is putting one IsInst expression to check and also creates one boolean variable to store the operation result.

Inference

Hence, basically, as everyone might thought, it is better to use Type constraint in generic argument. It is better to create a class or an interface, when you want your type to follow the Type parameter and put the constraint directly in the Type using where T:classname. In the above test, the former fails in terms or performance as the former builds up more IL and hence the constructor logic takes more time.

Download Sample Code - 30KB

Thanks for reading my post.

No comments:

Post a Comment

Please make sure that the question you ask is somehow related to the post you choose. Otherwise you post your general question in Forum section.