NDepend Blog

Improve your .NET code quality with NDepend

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

April 21, 2026 9 minutes read

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

Boxing in C# is the conversion of a value type (an int, a struct, an enum, etc.) into an object reference or an interface reference. The runtime wraps the value inside a heap-allocated object, and unboxing is the reverse operation that copies the value back out. Boxing is implicit, unboxing is explicit, and both cost more than they look: the extra heap allocation and the garbage collection pressure it creates can quietly slow down performance-critical code.

Boxing has been part of .NET since its inception two decades ago. In this post we’ll start by introducing boxing and unboxing and their performance cost, then move on to more modern and lesser-known topics: the everyday patterns that trigger boxing without you noticing, JIT escape analysis, tooling to detect boxing, ref struct and the .NET generics math library.

What Is Boxing and Unboxing in C#?

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.

Two details are worth underlining. Boxing is implicit: the plain assignment is enough to trigger it, no cast required. Unboxing is explicit and needs a cast back to the exact original value type. The boxed copy is independent of the source, which is why the outputs differ: incrementing i or value1.Value after the box leaves the boxed contents untouched. And because the cast must match the boxed type exactly, unboxing to the wrong type throws an InvalidCastException at runtime rather than performing a silent numeric conversion.

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

At a glance, here is how the two operations compare:

Boxing Unboxing
Direction value type -> object / interface object / interface -> value type
Conversion implicit explicit cast required
What happens allocates an object on the managed heap and copies the value into it verifies the type, then copies the value back to the stack
IL instruction box unbox / unbox.any
Main cost heap allocation and GC pressure type check and copy (cheaper, but not free)

The Performance Cost of Boxing in C#

Using BenchmarkDotNet we can assess the cost of boxing:

Here are the results:

BoxedToHeap() shows that boxing isn’t free: it burns CPU cycles and allocates memory on the heap (24 bytes here, just to hold a single int).

On its own, 24 bytes and a few nanoseconds are negligible. The trouble is scale. Box a value inside a loop that runs millions of times, or once per element of a large collection, and you generate a flood of short-lived heap objects. That is what drives garbage collection frequency up and produces the GC pauses that surface as latency spikes in a hot path. Boxing rarely hurts in isolation; it hurts in aggregate.

On the other hand BoxedToStack() is almost as fast as NoBoxing() despite a boxing and an 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 were a value type. This feature is named Object Stack Allocation. It results from JIT Escape Analysis Optimization and this is explained by Stephen Toub here.

Common Scenarios Where Boxing Happens in C#

Most boxing is not written on purpose. It hides behind ordinary-looking code, which is why it is so easy to ship and so hard to spot. These are the patterns that allocate boxed values most often in real projects:

  • Non-generic collections. Storing value types in ArrayList, Hashtable or the other System.Collections types boxes every element going in and unboxes it coming out. Replace them with List<T>, Dictionary<TKey, TValue> and friends.
  • Casting a struct to an interface. Assigning a struct to an interface variable, or calling an interface method on it (IComparable, IEnumerable, IDisposable, …), boxes the struct. That is exactly the second scenario shown earlier.
  • Value types in string concatenation and formatting. string.Concat, string.Format and any params object[] overload box each value-type argument you pass. Interpolated strings behave better on modern runtimes, but a value handed to a parameter typed as object still boxes.
  • Struct equality without IEquatable. Calling Equals or GetHashCode on a struct that doesn’t implement IEquatable<T> falls back to object.Equals(object), which boxes the argument. This bites hard when structs are used as dictionary or hash-set keys.
  • foreach over a non-generic IEnumerable. Enumerating a non-generic collection yields object, so each value-type element is boxed on every iteration.
  • Enums treated as objects. Storing an enum in an object field, or older Enum.HasFlag implementations, can box the enum value.

The single most common case is the non-generic collection. Compare the two snippets below: the generic version preserves the value type end to end and never touches the heap.

How to Detect Boxing and Unboxing 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.

Because boxing is encoded as the box and unbox.any IL instructions you saw earlier, any IL-aware tool can surface it. Decompilers such as ILSpy and dotPeek let you read the emitted IL and search for those instructions. At runtime, allocation profilers such as JetBrains dotMemory, the Visual Studio allocation tool and PerfView reveal the boxed allocations that pile up under load. Static analysis has the edge that it flags every occurrence at once, without running the code. NDepend is a .NET static analyzer and it can detect the usage of boxing and unboxing with a code rule like this one:

CSharp-Boxing-Detection-With-NDepend

You can refine such a rule to ignore cold code and only warn on boxing inside hot paths, which is where it actually matters.

How to Avoid Boxing 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. The same idea explains why implementing IEquatable<T> and IComparable<T> on your own structs removes boxing: the runtime can call the strongly typed methods instead of the object-based ones.

A common scenario where boxing and unboxing occurs unintentionally is when passing or returning value types through an 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 guarantees 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 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 a ref struct comes with the limitation that it cannot be stored in object fields.

C# Boxing FAQ

What is boxing in C#?

Boxing is the implicit conversion of a value type (such as int, double or a struct) into an object or interface reference. The CLR allocates an object on the managed heap and copies the value into it.

What is the difference between boxing and unboxing?

Boxing wraps a value type in a heap object and happens implicitly. Unboxing copies the value back out of that object into a value type and requires an explicit cast. Unboxing to a type that doesn’t match the boxed value throws an InvalidCastException.

Why is boxing bad for performance?

Every boxing operation allocates memory on the heap and copies the value. In loops and hot paths this produces large numbers of short-lived objects, which increases garbage collection pressure and causes latency spikes.

How do I avoid boxing in C#?

Use generics (List<int> instead of ArrayList), implement IEquatable<T> and IComparable<T> on your structs, prefer generic math (INumber<T>) over object parameters, and use ref struct or Span<T> where a type must never be boxed.

Is boxing always bad?

No. A handful of boxing operations are harmless. It only matters in performance-critical or high-throughput code, where the allocations accumulate and feed the garbage collector.

Do generics prevent boxing?

Yes. .NET reifies generics, so List<int> stores ints directly with no boxing, unlike Java, whose type erasure boxes value types in generic collections.

How can I find where boxing happens in my code?

Boxing is emitted as the box and unbox.any IL instructions, so a static analyzer such as NDepend, a decompiler such as ILSpy, or an allocation profiler such as dotMemory can pinpoint it.

Conclusion

Boxing can silently degrade the performance of critical code. Understanding when it occurs and how to avoid it lets you write more efficient .NET code. Features like generics, ref struct and modern JIT optimizations such as object stack allocation make it easier than ever to minimize unnecessary allocations and keep applications fast.

Another subtle but worthwhile 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!