Search code examples
c#.net-coreoptimization.net-9.0c#-13.0

Optimized Conversion from TSource to TDestination Using INumber<T> in .NET 9


I am working on a method to convert a numeric value of type TSource to another numeric type TDestination in a safe and optimized way. I am using .NET 9 and the generic INumber and IMinMaxValue interfaces introduced in .NET.

Here’s my current implementation:

public static TDestination ToNumberOrDefault<TSource, TDestination>(this TSource number, TDestination defaultValue)
    where TSource : struct, INumber<TSource>, IMinMaxValue<TSource>
    where TDestination : struct, INumber<TDestination>, IMinMaxValue<TDestination>
{
    try
    {
        return TDestination.CreateChecked(number);
    }
    catch 
    {
        return defaultValue;
    }
}

Problem: Performance Concern: The CreateChecked method appears to convert the source value to object internally before performing the conversion, which introduces boxing and unboxing overhead, making it less optimal. Frequent Usage: This method is called frequently in performance-critical code paths, so I'm looking for a faster alternative.

Code from Int32.cs as an Example:

else if (typeof(TOther) == typeof(long))
 {
     long actualValue = (long)(object)value;
     result = checked((int)actualValue);
     return true;
 }

Questions: Is there a more optimized way to perform this type conversion between TSource and TDestination while ensuring the value falls within the valid range of TDestination? Can I avoid the try/catch block and CreateChecked while still ensuring the value is safely converted or falls back to a default value if it's invalid? Are there alternative APIs or patterns in .NET 9 that are more suitable for this type of numeric conversion.

Constraints: TSource and TDestination are guaranteed to implement INumber and IMinMaxValue. I need to ensure that the value of TSource is within the range defined by TDestination.MinValue and TDestination.MaxValue before performing the conversion.

Example Usage:

var sourceValue = 12345;
var result = sourceValue.ToNumberOrDefault<int, short>(default(short)); // Should return short value or fallback to default

What I’ve Tried: Using CreateChecked: Works but introduces performance concerns due to boxing/unboxing. Using Convert.ChangeType: Similar issue with boxing/unboxing and potential overhead. Manually converting to an intermediate type (e.g., double or decimal) for range checks: Adds complexity and potential precision issues.


Solution

  • Problem: Performance Concern: The CreateChecked method appears to convert the source value to object internally before performing the conversion, which introduces boxing and unboxing overhead, making it less optimal

    There is no performance problem here (due to allocations at least) since there will be no allocations happening here, generics will be compiled for every different value type passed to the generic type parameter and JIT compiler is smart enough (at least in CLR) to not perform the extra work (boxing, casting, unnecessary checks etc.).

    The intermediate cast to object (i.e. in (long)(object)value;) is actually a quite common trick when working with the generics to cast generic to a concrete type after type check.

    You can easily check that your method will not actually allocate any memory:

    int i = 0;
    
    var totalAllocatedBytes = GC.GetTotalAllocatedBytes(true);
    
    for (int j = 0; j < 10_000; j++)
    {
        i = 100L.ToNumberOrDefault(1);
    }
    
    for (int j = 0; j < 10_000; j++)
    {
        i = 100.ToNumberOrDefault(1);
    }
    
    Console.WriteLine(GC.GetTotalAllocatedBytes(true) - totalAllocatedBytes);
    

    Which prints 0 for me.

    For actual performance tests I would recommend to use something like BenchmarkDotNet which has allocation diagnosis (via DotMemoryDiagnoser)

    Or check the JIT assembly for example via sharplab.io

    See also: