If, like me, you’re coding education was (ahem) “non-standard”, you might not have given much thought to the underlying mechanics of how .NET uses memory. Us C# developers are a bit spoilt really. We can pretty much get on with the coding, without having to concern ourselves with allocating memory or worrying about leaks. The CLR does an excellent job of sorting it all out behind the scenes.
It was only when boxing came up in a conversation recently that I realised how flakey my understanding of memory management in .NET really was. I mean, I sort of knew the concepts, but I think I thought I knew it better than it turned out I did. So I went away and brushed up and thought I might as well share my new understanding. In this post I’ll try to cover some basic concepts, such as value types vs reference types, the stack and the heap, and boxing and unboxing. I’ll finish by explaining both why you should care and why it’s less of an issue these days. In future posts, I may go a bit deeper into some of these concepts, using this post as a reference.
Value types and reference types
To understand boxing, we must first understand a little about how different kinds of types are stored. Some types, such as int, are of fixed size. An int is always 32 bits – 4 bytes of 8 bits each – in .NET. This means the maximum value for a (signed) int is 2,147,483,647: 2 to the power of 31 (32 – 1 bit for the sign). Other types, like string, can grow as needed, which makes it difficult to say how much space will be needed, ahead of assignment.
1 2 3 4 5 6 7 8 9 10 11 12 |
int x = int.MaxValue; Console.WriteLine(x); // 2,147,483,647 byte[] a = BitConverter.GetBytes(x); Console.WriteLine(a.Length); // 4 (always) string y = "Hello world!"; byte[] b = Encoding.ASCII.GetBytes(y); Console.WriteLine(b.Length); // 12 string z = "Hello world, it's great to meet you!"; byte[] c = Encoding.ASCII.GetBytes(z); Console.WriteLine(c.Length); // 36 |
Where the amount of memory is known and fixed, we call a type a value type. Otherwise, we call it a reference type. Examples of the former include all the basic numeric types, boolean, chars, enums, and structs (full list in the docs). Reference types are much more varied, by virtue of the fact that they have variable capacities. Importantly though, both strings and classes are reference types.
The stack and the heap
Since the amount of memory needed to store a value type will always be known ahead of time, these types can be stored very efficiently. Imagine I have a pile of sheets of paper. If I know that the thing I need to write will always take one (or some known multiple) sheet, I can set these aside and arrange them nicely in a pile. When I instantiate a value typed variable, I write the value onto a sheet of paper and add it to the pile. When I want to read the value, I flick to the correct sheet and there it is, with the most recent addition at the top and the oldest at the bottom. When I’m done with a particular job, I can discard the top few sheets that contain the variables I no longer need. This is the “stack”.
Now imagine that I need to store a string. I don’t yet know how long the string will be, so how can I determine how many sheets of paper to put on the pile? It’s impossible. Instead, what I can do it put a box file on a shelf. My box file can contain as many sheets of paper as I like. So I can find it again, I write the location of the box file on a sheet of paper, which can then go on the pile (the stack from the previous example). When I want to read my value, I flick to the sheet containing the location and use this to find the box file, which contains the value. In this metaphor, the shelf is the “heap”.

Shelves of box files. (Photo by seeminglee)
So, a value type has its value stored on the stack, whereas a reference type only has a reference to the location of its value stored on the stack.
So what is boxing?
OK, so imagine I tell you I’m going to give you a value, but I don’t say what type it is. What will you do? Would you start putting sheets on the pile? Or would you reach for a box file? Since you don’t know how much paper you’ll need, you’re forced to go for the box file. If I subsequently give you a value type, such as an int, you will have to place this in the box, on the shelf, as if it were a reference type. The CLR has to do the same thing. This is what we call boxing. Here’s an example:
1 2 |
int a = 5; object b = a; // boxing |
The int a will be placed onto the stack, but the object b will be put on the heap.
If I were to subsequently cast a boxed variable (e.g. my object b in the code above) into a value type, this would be unboxing. Simple right?
And why should you care?
In a word: performance. There’s a reason the CLR doesn’t just put everything on the heap – the overhead of putting a value on the heap and then the reference to this value on the stack incurs a performance penalty.
Consider the following:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
Stopwatch sw = new Stopwatch(); sw.Start(); for (int i = 0; i < 1000000; i++) { object j = i; } sw.Stop(); Console.WriteLine($"Boxing example took {sw.Elapsed.TotalMilliseconds}ms to complete"); sw.Reset(); sw.Start(); for (int i = 0; i < 1000000; i++) { int j = i; } sw.Stop(); Console.WriteLine($"Non-boxing example took {sw.Elapsed.TotalMilliseconds}ms to complete"); |
This code iterates over the first million integers and either boxes them into objects, or assigns them to another int. When I run this in Visual Studio it takes the version with boxing approximately 8ms, compared to the simple assignment, which only takes 3ms.
(Interestingly, running this in release mode dramatically reduces both times. I suppose because the example is so trivial it can be heavily optimised by the compiler. Anyway, the trend remains consistent.)
The impact of using reference types unnecessarily is a 100% increase in the time taken to complete the operation. That might not be a big deal for some applications, but for those that are operating at scale, or where performance is an issue, boxing is something it pays to be aware of.
Why it’s less of an issue these days
Without detracting from any of the above, when was the last time you cast an int to an object? Or, for that matter, an value type to any reference type?
It used to be more common. Back before we had generics, we had to make do with ArrayList instead of List<T>. An ArrayList is just a list of objects, so using one to store value types would automatically mean each item would be boxed and unboxed.
These days it’s rarer to deal with boxing directly. In fact, I was struggling to think of good examples. In the end, I put the question to the great community at Stack Overflow. Mike Nakis points out:
Boxing and unboxing is not something that you explicitly do; it is something that happens all the time whenever you have a struct in your hands and you are passing it to some receiver that expects an object.
He gives the example of methods that accept an object parameter. Passing a value type to such a methods will involve boxing, but in a less obvious way. This underscores the importance of being vigilant for instances of boxing.
Sinatr provided another situation when boxing may occur – when a List<object> is used in order to support lists of mixed types. This brings its own issues, as the underlying type (e.g. int) must be known in order to unbox, but there are some plausible use-cases.
Summary
Boxing and unboxing occurs when we transition between value types and reference types. These are handled differently in memory because the size of a reference type is not known at point at which we declare it. Value types normally use the stack, which is very efficient. Value types become boxed when they are placed inside a reference type, but this is undesirable due to the performance implications of using the heap. Boxing used to be quite common, but situations that give rise to it have largely been mitigated by the advent of generics.
This has been a whistlestop tour through some of the core concepts involved in .NET memory management. In future posts I’ll dive a bit deeper into some of these, as well as perhaps garbage collection (which I had to trim from this post). Stay tuned folks!