Hidden Costs of Boxing in C#: How to Detect and Avoid Them

C# Boxing and Unboxing are vital players in the performance of an application. However, they are often overlooked. They involve heap allocations that bring a penalty due to their accessing mechanism. In today's post, we will unfold Boxing and Unboxing in detail, study how they are costly to your application, and how to avoid such issues.

Hidden Costs of Boxing in C#: How to Detect and Avoid Them

What is Boxing and Unboxing in C#?

Boxing refers to converting a value type, such as intdouble , long, or struct into a reference type (e.g., object or interface). When the common language runtime (CLR) boxes a value type, it wraps the value inside a System. Object instance and stores it on the managed heap. Heap stores data for a long lifespan, and the Garbage Collector performs allocations and deallocations. In non-boxing, short-lived variables like local variables and method parameters use a stack that works on Last In, First Out (LIFO). Boxing implicitly converts value type variables to objects, as every type is a child of the Object class.

Unboxing is the opposite of Boxing, where value types are extracted from the object types. Unboxing requires explicit casting to the target type.

Boxing example

int count = 200;
object obj = count;

We boxed the count variable into the obj instance of type object.

Unboxing example

object obj = 204;
int count = (int) obj;

Now that we have understood what Boxing is, let's move on to the second part, which is how it affects your application.

Boxing project to evaluate performance difference with BenchmarkDotNet

I am creating a benchmark project to see the impact of Boxing against non-boxing scenarios. If you want to explore benchmarking and the BenchmarkDotnet library, make sure to read through this post: How to Monitor Your App's Performance with .NET Benchmarking.

Step 1: Create a project

dotnet new console -n BoxingBenchmark
cd BoxingBenchmark

Step 2: Install BenchmarkDotNet

dotnet add package BenchmarkDotNet

Step 3: Set up Benchmark runner

using BenchmarkDotNet.Attributes;

namespace BoxingBenchmark;

[MemoryDiagnoser]
public class BoxingBenchmarksRunner
{
    
    private int value = 222;

    [Benchmark]
    public object WithBoxing()
    {
        // Boxing: converting value type (int) into object
        return value;
    }

    [Benchmark]
    public int WithoutBoxing()
    {
        // No boxing: stays as int
        return value;
    }

    [Benchmark]
    public int WithUnboxing()
    {
        // Boxing + Unboxing: store as object and cast back
        object boxed = value;
        return (int)boxed;
    }
}

I defined 3 methods: Boxing, WithoutBoxing, and WithUnboxing for a simple int variable. With the Benchmark attribute, they will come under benchmarking. By default, BenchmarkDotnNet only evaluates time. Hence, at the top, I have specified MemoryDiagnoser to include memory in the benchmarking process as well.

Step 4: Run the benchmark runner in Program.cs

using BenchmarkDotNet.Running;
using BoxingBenchmark;

BenchmarkRunner.Run<BoxingBenchmarksRunner>();

Step 5: Build the solution

Benchmark requires a release environment. To release, we need to build the project first.

dotnet build

Step 6: Run release

Now running in release mode

dotnet run -c Release

Result

Benchmark output

We can clearly notice that Boxing not only slows down the execution but also consumes memory even for a single int variable. Without Boxing, a fast stack is used on the LIFO principle. While Boxing uses heap memory to store that value. The allocation and deallocation involve a garbage collector that removes the value from the heap when it is no longer needed. All this takes longer than stack push and pop. However, the garbage collector is itself a program that incurs CPU overhead, along with memory and time penalties. Stack uses optimized CPU cache for small value storage. However, the heap requires accessing slower memory, so it bypasses caching optimizations.

Boxing benchmark to evaluate ArrayList vs List

One of the most prominent examples of boxing you use without knowing is ArrayList. Yes, ArrayList works on boxing behind the scenes. Let's continue with the same project, showing how much using ArrayList and List can differ.

Step 1: Create a Benchmark runner

using System.Collections;
using BenchmarkDotNet.Attributes;

namespace BoxingBenchmark;

[MemoryDiagnoser]
public class AppointmentBenchmarksRunner
{
    private const int Count = 1000;

    [Benchmark]
    public void UsingArrayList()
    {
        var list = new ArrayList();
        for (int i = 0; i < Count; i++)
        {
            // Boxing happens here because ArrayList stores as object
            list.Add(i);

            // Unboxing when retrieving
            var a = (int)list[i];
        }
    }

    [Benchmark]
    public void UsingGenericList()
    {
        var list = new List<int>();
        for (int i = 0; i < Count; i++)
        {
            list.Add(i);

            // No boxing/unboxing
            var a = list[i];
        }
    }
}

We defined two methods here. The first one uses an ArrayList, and the second uses a generic List to save integer data. To maintain a considerable sample size, I performed 1000 iterations.

Step 2: Run the benchmark runner in Program.cs

using BenchmarkDotNet.Running;
using BoxingBenchmark;

BenchmarkRunner.Run<AppointmentBenchmarksRunner>();

Step 3: Build project

dotnet build

Step 4: Run the project in release

dotnet run -c Release

Result

Benchmark output

Step 5: Increase the iteration size

To observe the effect of increasing data, I am increasing the size to 10000.

private const int Count = 10_000;

Result

Benchmark output

We can observe that when the size of the data increased by 10 times, the speed of the generic list decreased in the same ratio. However, memory usage increased slightly. By the way, the change in resource usage was predictable. On the other hand, ArrayList took quite longer in execution, and its memory allocation was about four times that of the generic List. The List has no hidden allocations, hence it provides stable scaling. While ArrayList uses garbage Collector operations for each item, it is expensive.

Where did Boxing work in your project?

Boxing is in many places where you need to know and avoid. I have listed a few common scenarios where you knowingly or unknowingly use boxing.

Using non-generic collections (ArrayList, Hashtable)

var list = new ArrayList();
list.Add(26);                  // boxing
int firstValue = (int)list[0]; // unboxing

Generic collections with interface constraints

var list = new List<IComparable>();
list.Add(42); // boxing, because int stored as IComparable

Even if you are using a generic collection but with interface constraints, it utilizes boxing.

How to avoid Boxing in C#?

As it is expensive in many aspects, we should avoid falling into scenarios that use boxing implicitly or explicitly.

Use the Generics collections

As we have already seen in our examples, using generic collections for value types saves us from boxing pitfalls. Use List<T> or Dictionary<TKey, TValue> instead of a collection that relies on boxing, such as ArrayList.

Avoid unnecessary object casting

Wherever possible, avoid using System.Object variables in your project. System.Object require casting into value types that use boxing.

Use Object pooling

Instead of creating new objects every time you need them, create an object pool for heap-allocated objects. It allows you to reuse objects from the pools and relieve the garbage collector from frequent allocation and deallocation jobs. Object pooling technique is essential in real-time systems, high-throughput servers, and other performance-critical applications.

Conclusion

Boxing is a performance penalty that is hidden in a project if you don't choose the right thing for the right place. Mostly, it does not drive you to significant performance issues. However, if your application is very performance-critical and boxing gets involved in operations with extensive data, you can get in trouble. In this post, I shared insights about how much it can affect if you opt for the wrong collection or way of dealing with value types. We benchmarked performance for boxing and unboxing methods. We discussed where you might potentially rely on boxing without knowing. By following the guidance of this post, you can save your project from unnecessary resource usage and latency.

elmah.io: Error logging and Uptime Monitoring for your web apps

This blog post is brought to you by elmah.io. elmah.io is error logging, uptime monitoring, deployment tracking, and service heartbeats for your .NET and JavaScript applications. Stop relying on your users to notify you when something is wrong or dig through hundreds of megabytes of log files spread across servers. With elmah.io, we store all of your log messages, notify you through popular channels like email, Slack, and Microsoft Teams, and help you fix errors fast.

See how we can help you monitor your website for crashes Monitor your website