Boxing and Unboxing

In .NET we encounter value types, reference types and pointer types. Runtime handles these types in different ways.
Value types are allocated on the stack, while reference types are allocated on the heap, with a variable referencing it allocated on the stack.

Boxing and Unboxing - stack and heap

Since we want to have a unified type system that allows value types to have a completely different representation of their underlying data, we need to introduce a process that allows us to represent value types as reference types.
This process is called “Boxing”.

Boxing is an implicit conversion of a value type to the type object or to any interface type implemented by this value type. Boxing a value type allocates an object instance on the managed heap and copies the value into the new object.

The opposite is called “Unboxing”

Unboxing is an explicit conversion from the type object to a value type or from an interface type to a value type that implements the interface. An unboxing operation consists of:

  • Checking the object instance to make sure that it is a boxed value of the given value type.
  • Copying the value from the instance into the value-type variable.

Simplest example of code that involves Boxing and Unboxing can look like this:

object o = 123; // boxing int value "123"
int i = (int)o; //unboxing o

Let’s have a look at IL that was generated from the code above (sharplab example)
The following part is the most interesting to us:

Boxing and Unboxing - Generated IL 2

  1. We push “123” onto the stack as int32 using ldc.i4.s instruction.
  2. We box and unbox.any the value with System.Int32 argument.

Formal definition of these instructions in ECMA-335 standard at the beginning of chapter
III.4 Object model instructions.

Using Diagnostic Tools while debugging we can easily find that an object was allocated.

Boxing and Unboxing - Visual Studio Diagonostic Tools

“View Heap” shows us more detailed view.

Boxing and Unboxing - Visual Studio Managed Memory view

As we can see .NET Runtime allocated 24 Bytes on the heap to store our Int32 variable…
You may be thinking - “Why not 4 Bytes?”
It’s because CLR enforces minimal object size to leave a buffer for heap management and GC related data.
Minimal object size is 12 Bytes for x86, and 24 Bytes for x64.

Performance

Thanks to boxing and unboxing we can define methods accepting object derived reference types and still operate on value types.

Of course we have to pay for this benefit - with performance in this case.
Let’s say that we want to create a method that fills a list, and we decided to use List<object>.

    [SimpleJob(RuntimeMoniker.CoreRt30)]
    public class BoxingUnboxingBenchmark
    {
        const int Max = 1000;

        [Benchmark]
        public List<object> AddToListOfObjects()
        {
            var list = new List<object>();
            for (int i = 0; i < Max; i++)
            {
                list.Add(i);
            }

            return list;
        }

        [Benchmark]
        public List<int> AddToListOfInts()
        {
            var list = new List<int>();
            for (int i = 0; i < Max; i++)
            {
                list.Add(i);
            }

            return list;
        }
    }

How would that compare to correctly using List<int> ?

BenchmarkDotNet=v0.12.0, OS=Windows 10.0.18362
Intel Core i5-3320M CPU 2.60GHz (Ivy Bridge), 1 CPU, 4 logical and 2 physical cores
.NET Core SDK=3.0.100
  [Host]     : .NET Core 3.0.0 (CoreCLR 4.700.19.46205, CoreFX 4.700.19.46214), X64 RyuJIT
  Job-CBKJYW : .NET CoreRT 1.0.28316.01 @BuiltBy: dlab14-DDVSOWINAGE101 @Branch: master @Commit: 4fea75c6b158a8dee78acbba93d7e7c04f66984d, X64 AOT
Runtime=CoreRt 3.0
Method Mean Error StdDev
AddToListOfObjects 22.913 us 0.2393 us 0.2121 us
AddToListOfInts 3.442 us 0.1004 us 0.0939 us

In this exaggerated example, where boxing happens every time we add an element to the list, we can see that AddToListOfObjects is around 6.5x slower.

Conclusion

We should never optimize too early, but it is nice to know how our tools work under the hood.
Eliminating obvious unnecessary memory allocations may save us some debugging time in the future.