1


This C# code defines benchmark tests to compare the performance of boxing (using a non-generic number class) versus non-boxing (using a generic number class) operations on numeric values.




Date Added (UTC):

01 May 2024 @ 01:09

Date Updated (UTC):

01 May 2024 @ 01:09


.NET Version(s):

.NET 8

Tag(s):


Added By:
Profile Image

Blog   
Wilmington, DE 19808, USA    
A dedicated executive technical architect who is focused on expanding organizations technology capabilities.

Benchmark Results:





Benchmark Code:



using System;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;

public interface INumber
{
    object NumericValue { get; set; }
}

public class Number : INumber
{
    public object NumericValue { get; set; }
}

public interface INumber<T>
{
    T NumericValue { get; set; }
}

public class Number<T> : INumber<T>
{
    public T NumericValue { get; set; }
}

public class BoxingPerformanceBenchmark
{
    private readonly Number nonGenericNumber = new Number { NumericValue = 10 };
    private readonly Number<int> genericNumber = new Number<int> { NumericValue = 10 };

    [Benchmark]
    public void BoxingBenchmark()
    {
        var currentValue = nonGenericNumber.NumericValue;
        nonGenericNumber.NumericValue = 20;
    }

    [Benchmark]
    public void NonBoxingBenchmark()
    {
        var currentValue = genericNumber.NumericValue;
        genericNumber.NumericValue = 20;
    }
}

class Program
{
    static void Main(string[] args)
    {
        var benchmarkSummary = BenchmarkRunner.Run<BoxingPerformanceBenchmark>();
    }
}

// .NET 8
public void BoxingBenchmark()
{
    object numericValue = nonGenericNumber.NumericValue;
    nonGenericNumber.NumericValue = 20;
}
// .NET 8
public void NonBoxingBenchmark()
{
    int numericValue = genericNumber.NumericValue;
    genericNumber.NumericValue = 20;
}

// .NET 8
.method public hidebysig 
    instance void BoxingBenchmark () cil managed 
{
    .custom instance void [BenchmarkDotNet.Annotations]BenchmarkDotNet.Attributes.BenchmarkAttribute::.ctor(int32, string) = (
        01 00 29 00 00 00 01 5f 00 00
    )
    // Method begins at RVA 0x207a
    // Code size 31 (0x1f)
    .maxstack 8

    // sequence point: (line 44, col 9) to (line 44, col 58) in _
    IL_0000: ldarg.0
    IL_0001: ldfld class Number BoxingPerformanceBenchmark::nonGenericNumber
    IL_0006: callvirt instance object Number::get_NumericValue()
    IL_000b: pop
    // sequence point: (line 45, col 9) to (line 45, col 44) in _
    IL_000c: ldarg.0
    IL_000d: ldfld class Number BoxingPerformanceBenchmark::nonGenericNumber
    IL_0012: ldc.i4.s 20
    IL_0014: box [System.Runtime]System.Int32
    IL_0019: callvirt instance void Number::set_NumericValue(object)
    // sequence point: (line 46, col 5) to (line 46, col 6) in _
    IL_001e: ret
}
// .NET 8
.method public hidebysig 
    instance void NonBoxingBenchmark () cil managed 
{
    .custom instance void [BenchmarkDotNet.Annotations]BenchmarkDotNet.Attributes.BenchmarkAttribute::.ctor(int32, string) = (
        01 00 30 00 00 00 01 5f 00 00
    )
    // Method begins at RVA 0x209a
    // Code size 26 (0x1a)
    .maxstack 8

    // sequence point: (line 51, col 9) to (line 51, col 55) in _
    IL_0000: ldarg.0
    IL_0001: ldfld class Number`1<int32> BoxingPerformanceBenchmark::genericNumber
    IL_0006: callvirt instance !0 class Number`1<int32>::get_NumericValue()
    IL_000b: pop
    // sequence point: (line 52, col 9) to (line 52, col 41) in _
    IL_000c: ldarg.0
    IL_000d: ldfld class Number`1<int32> BoxingPerformanceBenchmark::genericNumber
    IL_0012: ldc.i4.s 20
    IL_0014: callvirt instance void class Number`1<int32>::set_NumericValue(!0)
    // sequence point: (line 53, col 5) to (line 53, col 6) in _
    IL_0019: ret
}

// .NET 8 (X64)
BoxingBenchmark()
    L0000: push rbx
    L0001: sub rsp, 0x20
    L0005: mov rbx, [rcx+8]
    L0009: mov ecx, [rbx+8]
    L000c: mov rcx, 0x7ffe51981188
    L0016: call 0x00007ffeb157ae10
    L001b: mov dword ptr [rax+8], 0x14
    L0022: lea rcx, [rbx+8]
    L0026: mov rdx, rax
    L0029: call 0x00007ffeb157a680
    L002e: nop
    L002f: add rsp, 0x20
    L0033: pop rbx
    L0034: ret
// .NET 8 (X64)
NonBoxingBenchmark()
    L0000: mov rax, [rcx+0x10]
    L0004: mov ecx, [rax+8]
    L0007: mov dword ptr [rax+8], 0x14
    L000e: ret


Benchmark Description:


When using non-generic interfaces, value types like int are wrapped in an object during assignments and retrievals. This conversion (boxing) and its reversal (unboxing) add overhead. Generic classes eliminate boxing by explicitly defining the data type. This allows direct manipulation of the value, like int, leading to significant performance gains. Our code demonstrates a whopping 67x speedup using generics! For optimal performance, leverage generics when you know the data type at compile time. It saves you time and improves code efficiency!

### General Setup Overview The provided benchmark code is designed to be executed using the BenchmarkDotNet library, a powerful tool for benchmarking .NET code. The benchmarks are focused on comparing the performance implications of boxing and non-boxing operations in .NET. Although the .NET version is not explicitly mentioned, the code is compatible with .NET Core 2.1 and above, including .NET 5 and .NET 6, due to the usage of BenchmarkDotNet and the coding patterns involved. **Configuration:** - **BenchmarkDotNet:** A benchmarking library that provides a robust framework for executing performance tests in .NET. It handles the setup, execution, and analysis of benchmarks, offering detailed results and statistics. - **.NET Version:** While not specified, the code should be run on a .NET Core 2.1+ environment to ensure compatibility with BenchmarkDotNet and modern C# features. ### Benchmark Methods Overview #### 1. `BoxingBenchmark` **Purpose:** This method is designed to measure the performance impact of boxing operations in .NET. Boxing is the process of converting a value type (e.g., an `int`) to an object type or to any interface type implemented by this value type. In this benchmark, `NumericValue` is of type `object`, so assigning an `int` to it causes boxing. **Performance Aspect Tested:** It tests how long it takes to perform a boxing operation when setting a value type to a property of type `object`. This is important because boxing involves creating a new object on the heap and copying the value into it, which can be a costly operation in terms of both time and memory, especially in tight loops or performance-critical sections of code. **Expected Insights:** Running this benchmark will help understand the overhead introduced by boxing operations. Higher execution times and more allocations are expected compared to non-boxing operations. #### 2. `NonBoxingBenchmark` **Purpose:** This method aims to measure the performance when using generic types to avoid boxing. Generics allow for type-safe data containers that do not require boxing when dealing with value types, as the type is known at compile time. **Performance Aspect Tested:** It evaluates the efficiency of accessing and setting a value on a strongly-typed generic property compared to a boxing operation. This is crucial for understanding the benefits of using generics to optimize performance-critical code paths. **Expected Insights:** The expectation is that this benchmark will demonstrate significantly lower execution times and fewer memory allocations compared to the boxing benchmark. This highlights the performance benefits of using generics to avoid the overhead of boxing. ### Conclusion The contrast between these two benchmarks will provide clear insights into the cost of boxing in .NET and underscore the importance of using generics to optimize performance. The results should guide developers in making informed decisions about structuring their data access and manipulation code, especially in scenarios where performance is critical. Understanding the implications of these benchmarks can lead to more efficient and performant .NET applications.


Benchmark Comments: