NDepend Blog

Improve your .NET code quality with NDepend

Boxing in C#: What It Costs You and How to Get Rid of It

October 22, 2025 5 minutes read

Boxing in C# What It Costs You and How to Get Rid of It

Boxing in C# happens when a value (int, struct, etc.) is converted to an object reference or an interface reference. This wraps the value in a heap-allocated object, which adds overhead. In performance-critical code, boxing can slow execution both due to the extra allocation and the increased work for the garbage collector.

Boxing has been part of .NET since its inception two decades ago. In this post, we’ll start by introducing boxing and its performance cost, then cover more modern and lesser-known topics such as JIT escape analysis, tooling for detecting boxing, ref struct and the .NET generics math library.

Boxing and Unboxing

Here’s a sample program demonstrating the two main scenarios where C# performs boxing and unboxing:

  1. When a value is boxed into an object reference.

  2. When a value is boxed by casting it to an interface that its value type implements.

Here is the decompiled IL code illustrating the use of the box and unbox.any instructions:

The Cost of Boxing

Using Benchmark.NET we can asses the cost of boxing:

Here are the results:

BoxedToHeap() shows that boxing isn’t free—it consumes CPU cycles and allocates memory on the heap.

On the other hand BoxedToStack() is almost as fast as NoBoxing() despite a boxing and unboxing operation. This is because since .NET 9 the JIT is smart enough to detect that the reference doesn’t escape the method scope.  As a consequence it can allocate the wrapping object on the stack, as if it was a value type. This feature is name Object Stack Allocation. It results from JIT Escape Analysis Optimization and this is explained by Stephan Toub here.

Identifying Where Boxing and Unboxing Occur in Your Code

Although boxing occurs at runtime, the C# compiler dictates it by emitting specific IL instructions. This means you don’t need to run the code to detect it—it can be identified through static analysis. NDepend is a .NET static analyzer and it can detect the usage of boxing and unboxing with such code rule:

CSharp-Boxing-Detection-With-NDepend

Getting Rid of Boxing and Unboxing with Generics

The good news is that boxing and unboxing can often be avoided in C# thanks to .NET generics, which preserve value types at compile time. In contrast, Java’s generics use type erasure, so value types are still boxed when stored in generic collections.

For example, in C# a List<int> stores values directly, while in Java a List<Integer> stores boxed integers. Since generics were introduced in .NET 2.0, they have given the .NET platform a performance advantage over Java in handling value types.

A common scenario where boxing and unboxing occurs unintentionally is when passing or returning value types through object reference as method parameters:

The method above may look awkward. Unfortunately it’s not uncommon to see object used where a more specific type would be appropriate—consider, for example, Equals<object>(object other).

Thanks to the .NET Generics math library, this Increment(object):object method can be rewritten this way:

Although all primitive numeric types (int, double, byte, etc.) implement the INumber<TSelf> interface, constraining a generic type to INumber<TSelf> does not generate any boxing or unboxing IL instructions. This works because, at runtime, an optimized version of Increment<T>() is generated for each value type T it’s called with. For reference types T, the JIT can often share the same compiled code. This is a remarkable design feature of .NET that you should be aware of.

When I first explored the .NET generics math library, my reaction was: “What? Primitive types now implement plenty of interfaces—INumber<TSelf> alone extends 18 interfaces!” Does this hurt performance? Not at all—generics in .NET are carefully designed to avoid any boxing and any overhead.

Interfaces on structs matter—and using them wisely can even improve your application’s performance.

Preventing Boxing at compile-time with ref struct

To implement Span<T>, C# introduced the concept of a ref struct. A ref struct is a struct that the compiler ensures will never be boxed or placed in an object field. This restriction exists because Span<T> holds internally something special through a ref field, a managed pointer. A managed pointer is a high-performance reference that must remain on the stack to be safely tracked by the garbage collector. Managed pointers are awesome because they can track any kind of memory location: Managed pointers are powerful because they can reference any exotic memory location: a field inside an object, an element in a collection, or a character at a specific index in a string.

Managed pointers have existed since the early days of .NET with the by ref parameter, but they gained new prominence with Span<T> and ref struct. This is why we use the ref keyword in ref struct, ref field, and ref return—even though it has nothing to do with a regular reference type. This blog post explains it all: Managed pointers, Span, ref struct, C#11 ref fields and the scoped keyword

As a result, using a ref struct forces the compiler to ensure its values are never boxed because they cannot live on the heap.

ref struct and boxing

Nevertheless keep in mind that ref struct comes with the limitation that they cannot be stored in object fields.

Conclusion

Boxing can silently degrade performance of critical code. Understanding when it occurs and how to avoid it allows you to write more efficient .NET code. .NET features like generics, ref struct and some modern JIT optimizations make it easier than ever to minimize unnecessary allocations and maintain high-performance applications.

Another subtle but worth knowing modern optimization to be aware of is C# 13’s params collections.

This article is brought to you by the team behind NDepend — a proven .NET static analysis tool for improving code maintainability, security, and overall quality. Whether you’re modernizing a legacy .NET application or starting fresh in C#, get started with your free full-featured trial today!

Leave a Reply

Your email address will not be published. Required fields are marked *