Even though with the .NET framework we don't have to actively worry about memory management and garbage collection (GC), we still have to keep memory management and GC in mind in order to optimize the performance of our applications. One of the things we need to be aware of is how the Common Language Runtime (CLR) deals with references to value types.
When an instance of a value type is converted to a System.Object type or to an interface, the CLR needs to convert the value type to a valid reference type. Memory is allocated on the managed heap and the object is copied over. We need to be aware of this for two reasons: boxing is a very expensive process (copying a whole object from the stack to the heap uses up processor cycles and space on the managed heap) and we now have two objects in memory that can have conflicting states.
Here is a simple example of boxing.
int i = 0;
object obj = (object) i; // i is boxed here
obj = ((int) i) + 3; // changing i on the heap
i += 1; // changing i on the stack
Console.WriteLine(obj.ToString());
Console.WriteLine(i.ToString());
Gives us the result:
Here is a play-by-play of what happens
Step 1: i is placed on the stack
Step 2: i is copied to the heap (boxed) and obj goes on the stack, the value of obj is the memory address of the new object on the heap (a pointer).
Step 3: The value of obj is updated and now contains the value 3
Step 4: i (the one on the stack) is updated and now contains the value 1
Here is a more complex example:
class Class1
{
[STAThread]
static void Main(string[] args)
{
MyInt test = new MyInt();
test.value = 100;
test.AddOne();
aDelegate del = new aDelegate(test.AddOne);
del();
test.AddOne();
}
}
public delegate void aDelegate();
public struct MyInt
{
public int value;
public void AddOne()
{
Console.WriteLine(string.Format("Before: {0}", value.ToString()));
value += 1;
Console.WriteLine(string.Format("After : {0}", value.ToString()));
}
}
Here's the output:
Not the result you were expecting, right? What's going on? Let's walk through what happens. After Main() and args[] are placed on the stack and the Main method begins execution:
Step 1: test is placed on the stack
Step 2: AddOne() is placed on the stack and executed. Changing the value of MyInt.value to 101;
Step 3: AddOne() is removed from the stack and MyInt is boxed (copied) to the heap. Del now points to the heap version of the object.
Step 4: AddOne() is called from the boxed object, updating it's value to 102.
Step 5: AddOne() from the heap is removed.
Step 6: AddOne() from the stack version of MyInt is added to the stack, changing it's value to 102.
Finally: Main is done executing and cleared off the stack. The object left in the heap is ready for Garbage Collection (GC).
Let's make a change to improve the performance of our application and get results that make more sense. If we make MyInt into a reference type, we'll be dealing with one object on the heap and there will be no boxing.
public class MyInt
{
public int value;
public void AddOne()
{
Console.WriteLine(string.Format("Before: {0}", value.ToString()));
value += 1;
Console.WriteLine(string.Format("After : {0}", value.ToString()));
}
}
With this change we get results closer to what we are expecting:
Before: 100 After : 101 Before: 101 After : 102 Before: 102 After : 103
|
In Conclusion
More unexpected places boxing shows up is when we call ToString() or GetType() on a value type. In this case the method is called through the base object (System.Object) and therefore boxing is required before the method is accessible. Boxing also shows up when we place value objects into an ArrayList() (in which case they are typecast to System.Object). As you can imagine, the boxing process can be very expensive if done repeatedly on a large scale.
Hopefully this article gave you a better understanding of how value types are boxed and what to watch out for when dealing with value types through references like interfaces, delegates, or when they are put in an ArrayList.
Until next time,
-Happy coding