Back to Blog
C#

C# Boxing Performance: Avoiding Hidden Allocations

c# boxing performance: Learn how boxing affects C# performance, where hidden allocations occur, and how generics help you avoid them in hot paths.

boxingunboxinggenericsmemory allocationperformance
Diagram showing a value type being boxed into a heap object with an arrow indicating the allocation overhead.

c# boxing performance requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.

When a value type is assigned to a reference type variable, the runtime allocates a new object on the heap and copies the value into it. That process is boxing, and it is one of the most common sources of unexpected allocations in C#. For code that runs frequently, the cost of boxing can dominate execution time. Understanding where boxing happens and how to prevent it is a practical skill for any C# developer working on performance-sensitive applications.

What Boxing Does to Value Types

Boxing converts a value type, such as int, struct, or enum, into a reference type instance. The runtime creates a new object on the managed heap, copies the value into that object, and returns a reference. Unboxing is the reverse: it extracts the value from the boxed object and copies it back into a value type variable.

int number = 42; object boxed = number; // boxing: allocation on the heap int unboxed = (int)boxed; // unboxing: copy back

The boxed variable refers to a heap object that contains the value 42. That object is not free; it requires memory and must be garbage collected later. The unboxing operation itself is relatively cheap, but the allocation and subsequent collection are not.

Why Boxing Costs More Than a Cast

A cast from int to object looks like a simple type conversion, but it is not a no-op. The runtime must allocate memory, copy the value, and maintain type information. In a tight loop, these allocations add pressure on the garbage collector and can cause frequent GC pauses.

Consider this loop that sums integers using an ArrayList:

ArrayList list = new ArrayList(); for (int i = 0; i < 1000; i++) { list.Add(i); // each Add boxes the int }

Every call to Add boxes the integer. The ArrayList stores elements as object, so the value type int is converted on the heap. The same pattern appears when using non-generic collections, Hashtable, or any API that accepts object.

Common Boxing Sources in Everyday Code

Boxing often appears in places you might not notice. String concatenation with value types is a classic example:

string message = "Value: " + 42; // boxing occurs here

The + operator calls string.Concat(object, object), which boxes the integer. The same happens when you pass a value type to a method that expects object, or when you use Enum in string interpolation without an explicit cast.

Another frequent source is calling ToString() on a value type through an interface or base class reference. For example:

IFormattable formattable = 42; // boxing string text = formattable.ToString("D", null);

When you assign a value type to an interface it implements, the runtime boxes it because interfaces are reference types.

Using Generics to Avoid Boxing

Generic types and methods maintain type safety without boxing. A List<int> stores the integers directly in an internal array, not as boxed objects. The generic constraint system lets you write code that works with value types without converting them to object.

List<int> numbers = new List<int>(); for (int i = 0; i < 1000; i++) { numbers.Add(i); // no boxing }

Similarly, generic methods allow you to operate on value types without boxing:

public static T Max<T>(T a, T b) where T : IComparable<T> { return a.CompareTo(b) >= 0 ? a : b; }

Here T can be int, double, or a custom struct. The runtime generates a specialized version of the method for each value type, so the comparison is performed directly on the value, not on a boxed copy.

When Boxing Is Unavoidable

Some APIs are designed around object and cannot be avoided without changing the interface. For example, ArrayList, Hashtable, and Queue are non-generic and force boxing when used with value types. In modern C#, you should prefer their generic counterparts: List<T>, Dictionary<TKey, TValue>, and Queue<T>. But if you must interoperate with a legacy API that accepts object, boxing is the price you pay.

Another unavoidable case is reflection. When you invoke a method via reflection, the arguments are passed as object[], so value types are boxed. This is one reason reflection is slow. If you need high performance, consider using expression trees or source generators to avoid reflection overhead.

Measuring and Profiling Boxing in Production

Boxing is not always visible in a profiler as a distinct line item, but you can detect it by looking for allocations. Tools like the .NET memory profiler, PerfView, or the dotnet-counters tool can show allocation rates and GC pressure. In a production environment, a sudden increase in allocation rate often correlates with boxing in a hot path.

You can also use the IL view of your compiled code. The box instruction appears explicitly when boxing occurs. For example, the IL for object boxed = 42; contains box System.Int32. Searching for box in the disassembly helps identify unexpected conversions.

A practical approach is to write a small benchmark using BenchmarkDotNet to compare generic and non-generic versions of the same operation. The allocation column will show the difference clearly. For instance, a method that adds to a List<int> should report zero allocations, while one that adds to an ArrayList will report one allocation per item.

Struct Design and Boxing Pitfalls

Custom structs can also introduce boxing when they implement interfaces. If a struct implements IComparable and you call CompareTo through an interface reference, the struct is boxed. To avoid this, use the generic IComparable<T> interface and ensure your methods accept the concrete type or a generic parameter.

struct Point : IComparable<Point> { public int X; public int Y; public int CompareTo(Point other) { int xComparison = X.CompareTo(other.X); return xComparison != 0 ? xComparison : Y.CompareTo(other.Y); } }

When you call point.CompareTo(other) directly on a Point variable, no boxing occurs. But if you assign the struct to an IComparable variable, the runtime boxes it. Always prefer the generic interface and avoid casting structs to non-generic interfaces in performance-critical code.

The Cost of Unboxing and Type Checks

Unboxing itself is not free. It requires a type check and a copy. The type check can throw an InvalidCastException if the object is not of the expected type. This is another reason to avoid boxing: it introduces runtime type checks that are unnecessary when the type is known at compile time.

object boxed = 42; long value = (long)boxed; // throws InvalidCastException

The cast from object to long fails because the boxed type is int, not long. This is a common source of runtime errors. Generic code avoids this entirely because the type is fixed at compile time.

Production Considerations for Hot Paths

In high-throughput services, boxing can become a bottleneck. Every allocation increases GC pressure, and if the allocation rate is high enough, the garbage collector will run more frequently, causing latency spikes. When you profile a service and see a large number of small object allocations, boxing is often the culprit.

To reduce boxing in production, review your code for:

  • Non-generic collections in legacy code paths
  • Methods that accept object or dynamic
  • String interpolation with value types
  • Calls to ToString() on value types through interface references
  • Reflection-based invocation of methods with value type arguments

Each of these can be replaced with a generic alternative or a direct call. The effort is usually small, but the payoff in reduced GC pressure can be significant.

A final note: do not assume that all boxing is harmful. In a one-time operation, the allocation is negligible. The cost only matters when the operation is repeated many times or in a loop. Focus your optimization efforts on code that runs frequently, such as request handlers, data processing loops, and rendering pipelines.

c# boxing performance: Practical Usage and Code Examples | RYUSLOG DEV