Effectively Using Value Types in .Net

The .Net type system offers the following two kinds of types:

  • Reference Types
  • Value Types
Reference types are nothing but I can say any class you are writing is a reference type getting allocated on the Heap. So, on reference types we can apply OOP methodology, locking and many other methods.

However, in Value types you can't get that flexibility. Also, value types are allocated on the stack, avoiding pressure on the garbage collector. You should choose value types wisely, this means that only choose value types when your application is performance critical, otherwise Value types may include a variety of complexities later in the program. Consider a simple example shown below.

The following program is fairly simple. First, I allocated 10 million instances of int on the stack and then tried to reach the last element in the list. I also repeated that step 25 times and at that time I captured the GC instances and time elapsed. Normally, we can write the following program.
  1. using System;    
  2. using System.Collections.Generic;    
  3. using System.Diagnostics;    
  4. using System.Linq;    
  5. using System.Text;    
  6. using System.Threading.Tasks;    
  7.     
  8. namespace ValueTypes    
  9. {    
  10.     class Program    
  11.     {    
  12.         private const int noOfAllocations = 10000000;    
  13.         private const int repeats = 20;    
  14.     
  15.         static void searchAllocationinList<TAllocation>(string desc, Func<int, TAllocation> allocator)    
  16.             where TAllocation:struct     
  17.         {    
  18.             List<TAllocation> allocations = new List<TAllocation>();    
  19.     
  20.             //add 10 million ints in the list    
  21.             for (int i = 0; i < noOfAllocations; i++)    
  22.             {    
  23.                 allocations.Add(allocator(i));    
  24.             }    
  25.     
  26.             //find the last one    
  27.             TAllocation findAllocation = allocator(noOfAllocations);    
  28.     
  29.             int GCGeneration = GC.CollectionCount(0);    
  30.     
  31.             Stopwatch stopwatch = Stopwatch.StartNew();    
  32.     
  33.             for (int i = 0; i < repeats; i++)    
  34.             {    
  35.                 allocations.Contains(findAllocation);    
  36.             }    
  37.             stopwatch.Stop();    
  38.     
  39.             Console.WriteLine("{0} Average time per lookup: {1:0.00}ms\n\tGarbage collections: {2}",    
  40.                 desc, stopwatch.ElapsedMilliseconds , GC.CollectionCount(0) - GCGeneration);    
  41.         }    
  42.         static void Main(string[] args)    
  43.         {    
  44.             searchAllocationinList("Sample Run",i=>new Sample1{x = i,y=i});    
  45.             Console.ReadLine();    
  46.         }    
  47.     }    
  48. }   
  1. namespace ValueTypes    
  2. {    
  3.     struct Sample1    
  4.     {    
  5.         public int x;    
  6.         public int y;    
  7.     }    
  8. }   
Now, when I run that, it will produce the following results:
 
screen shot

Now, in the second version, what I will do is override the equals operation, so it does suppress the use of reflection at runtime, but still involves one boxing operation for comparison.
  1. using System;    
  2. using System.Collections.Generic;    
  3. using System.Linq;    
  4. using System.Text;    
  5. using System.Threading.Tasks;    
  6.     
  7. namespace ValueTypes    
  8. {    
  9.     struct Sample2    
  10.     {    
  11.         public int x;    
  12.         public int y;    
  13.     
  14.         public override bool Equals(object obj)    
  15.         {    
  16.             if(!(obj is Sample2)) return false;    
  17.             Sample2 obSample2 = (Sample2) obj;    
  18.             return x == obSample2.x && y == obSample2.y;    
  19.         }    
  20.     }    
  21.     
  22. }   
Now, when I invoke it as in the following code:
  1. static void Main(string[] args)    
  2. {    
  3.     searchAllocationinList("Sample Run",i=>new Sample1{x = i,y=i});    
  4.     searchAllocationinList("Sample Run 2",i=>new Sample2{x=i,y=i});    
  5.     Console.ReadLine();    
  6. }   
It will produce the following result. It does improve the performance.

performance

Now, let's see another example with IEquatable implementation.
  1. using System;    
  2.     
  3. namespace ValueTypes    
  4. {    
  5.     struct Sample3 :IEquatable<Sample3>    
  6.     {    
  7.         public int x;    
  8.         public int y;    
  9.     
  10.         public bool Equals(Sample3 objSample3)    
  11.         {    
  12.             if (!(objSample3 is Sample3)) return false;    
  13.             Sample3 obSample2 = (Sample3)objSample3;    
  14.             return x == obSample2.x && y == obSample2.y;    
  15.         }    
  16.     }    
  17.     
  18. }   
  1. static void Main(string[] args)    
  2. {    
  3.     searchAllocationinList("Sample Run",i=>new Sample1{x = i,y=i});    
  4.     searchAllocationinList("Sample Run 2",i=>new Sample2{x=i,y=i});    
  5.     searchAllocationinList("Sample Run 3", i => new Sample3 { x = i, y = i });    
  6.     Console.ReadLine();    
  7. }   
It will produce the following optimized result.

result

Therefore, the Contains method checks for equality in the list and if we have a collection like the preceding one then it would be a big performance issue. Now, with an IEquatble implementation the job is done and done faster. This interface has just one method, the equals method, that takes a value type as a parameter. Hence, rather than relying on the default implementation of object.valueType, this kind of approach will be a real performance booster. Thanks for joining me.


Similar Articles