NDepend Blog

Improve your .NET code quality with NDepend

The .NET Generic Math Library

May 15, 2024 7 minutes read

In November 2022 with the release of .NET 7, new math-related generic interfaces have been added to the .NET Base Class Library (BCL). This is named .NET Generic Math. These interfaces allow you to constrain a type parameter in a generic type or method to behave like a number.

Abstracting Algorithm with Generic Math

.NET Generic Math makes it possible to perform mathematical operations generically, meaning you don’t need to know the exact type you’re working with. For example, this method Middle<T>() abstracts the computation of the middle of two numbers:

This small method doesn’t look like a valid C# code for two reasons:

  • The expression T.One is a call on a static member defined on a generic parameter type.
  • The expression (a + b) / two calls the addition and division operators overloaded on the generic parameter type T.

How does this work?

Interface static abstract members

This is possible because T is required to implement the Generic Math interface System.Numerics.INumber<T>.  This interface is special because it has static abstract members. They were introduced in C# 11 and .NET 7 specifically to enable the implementation of Generic Math.

More specifically the interface INumber<TSelf> extends the interface  INumberBase<T> defined as:

The interface INumber<TSelf> extends the interfaces IAdditionOperators<TSelf,TOther,TResult> and IDivisionOperators<TSelf,TOther,TResult> defined as:

Generic Math as a Collection of Fine-Grained Interfaces

We saw that the interface INumber<TSelf> declared in the namespace System.Numerics plays a central role in Generic Math. Generic Math introduces many new fine-grained interfaces to implement every aspect of numerics.

Numeric Interfaces

Numeric interfaces abstract numeric representations like INumber<TSelf>. INumber<TSelf> mostly represent real number types. It extends INumberBase<TSelf> that defines the base for complex and real number types:

With a code query on the .NET BCL, we can see which structures implement INumber<TSelf> and which interfaces extend it:

INumber derived

Here are all the numeric interfaces defined in the System.Numerics namespace:

Interface Description
INumberBase<TSelf> Abstracts the representation and APIs of all number types, typically real and complex numbers. It exposes properties One and Zero. It exposes the method CreateChecked<TOther>(TOther) that could have been used in our Middle() example above to create the value 2 through T.CreateChecked(2). It exposes other methods like IsComplexNumber(TSelf), IsRealNumber(TSelf), IsInteger(TSelf), IsPositive(TSelf), IsZero(TSelf), IsNan(TSelf) or IsInfinite(TSelf). It is implemented by all numbers shown in the screenshot above, including also the structure Complex.
INumber<TSelf> Abstracts the representation and APIs of comparable number types, typically only real numbers. It proposes methods like MaxNumber(TSelf, TSelf) or Sign(TSelf).
IBinaryNumber<TSelf> Abstracts the representation and APIs of binary numbers. It proposes the property AllBitsSet and methods like IsPow2(TSelf) and Log2(TSelf).
IBinaryInteger<TSelf> Abstracts the representation and APIs of binary integers. It presents methods like GetByteCount().
IFloatingPoint<TSelf> Abstracts the representation and APIs of floating-point types like float, double, decimal, System.Half (16 bits floating numbers) and NFloat.
IFloatingPointIeee754<TSelf> Abstracts the representation and APIs of floating-point types that implement the IEEE 754 like the ones above except decimal.
IBinaryFloatingPointIeee754<TSelf> Same as above with an emphasis on binary floating-point representation in the IEEE 754 standard. It presents methods like GetExponentByteCount() or GetSignificandBitLength().
IFloatingPointConstants<TSelf> Exposes properties E, Pi and Tau.
ISignedNumber<TSelf> Abstracts the representation and APIs of all signed number types through its property T.NegativeOne.
IUnsignedNumber<TSelf> Abstracts the representation and APIs of all unsigned number types.
IAdditiveIdentity<TSelf,TResult> It only exposes T.AdditiveIdentity useful in (x + T.AdditiveIdentity) == x.
IMultiplicativeIdentity<TSelf,TResult> It only exposes T.MultiplicativeIdentity useful in (x * T.MultiplicativeIdentity) == x.
IMinMaxValue<TSelf> Exposes T.MinValue and T.MaxValue.

Operator interfaces

Operator interfaces each propose an operator available to the C# language.

  • They intentionally avoid pairing operations like multiplication and division, as this isn’t appropriate for all types. For instance, Matrix3x3 * Matrix3x3 is valid but Matrix3x3 / Matrix3x3 isn’t valid.
  • Also, an interface like IMultiplicationOperators<TSelf,TOther,TResult> has 3 generic parameter types for inputs and the result. For instance, Matrix3x3 * double is valid, and multiplying two sbyte returns an int: 255 * 255 = 65025

sbyte Multiply

Interface Corresponding C# operators
IAdditionOperators<TSelf,TOther,TResult> x + y
ISubtractionOperators<TSelf,TOther,TResult> x - y
IMultiplyOperators<TSelf,TOther,TResult> x * y
IDivisionOperators<TSelf,TOther,TResult> x / y
IEqualityOperators<TSelf,TOther,TResult> x == y and x != y
IComparisonOperators<TSelf,TOther,TResult> x < y, x > y, x <= y, and x >= y
IIncrementOperators<TSelf> ++x and x++
IDecrementOperators<TSelf> --x and x--
IModulusOperators<TSelf,TOther,TResult> x % y
IBitwiseOperators<TSelf,TOther,TResult> x & y, ‘x | y’, x ^ y, and ~x
IShiftOperators<TSelf,TOther,TResult> x << y and x >> y
IUnaryNegationOperators<TSelf,TResult> -x
IUnaryPlusOperators<TSelf,TResult> +x

Function interfaces

Function interfaces abstract usual mathematical functions. These interfaces are implemented by floating number structures like float, double, decimal, System.Half and NFloat.

Interface Description
IExponentialFunctions<TSelf> Abstracts exponential functions: Exp(TSelf)Exp10(TSelf), Exp2(TSelf)
ILogarithmicFunctions<TSelf> Abstracts logarithmic functions: Log(TSelf), Log10(TSelf), Log2(TSelf)
ITrigonometricFunctions<TSelf> Abstracts trigonometric functions: Cos(TSelf), Sin(TSelf), Tan(TSelf), Acos(TSelf), Asin(TSelf), Atan(TSelf)
IPowerFunctions<TSelf> Abstracts the power function:  Pow(TSelf,TSelf).
IRootFunctions<TSelf> Abstracts root functions: Sqrt(TSelf), RootN(TSelf,Int32)
IHyperbolicFunctions<TSelf> Abstracts hyperbolic functions: Cosh(TSelf), Sinh(TSelf), Tanh(TSelf)Acosh(TSelf), Asinh(TSelf), Atanh(TSelf),

The methods presented by these interfaces are static abstract methods and can be used through code like this double.Exp(2).

Notice that for compatibility reasons these functions are still implemented by the System.Math class, so we have a doublon: Assert.IsTrue(double.Exp(2) == Math.Exp(2)).

Parsing and formatting interfaces

Formatting involves converting a number into a human-friendly representation, making it easier to read and understand. Conversely, parsing is the reverse process, where textual data is converted back into a numerical format for computational use.

Notice how generic parsing interfaces propose static abstract members.

Interface Description
IParsable<TSelf> Abstracts T.Parse(string, IFormatProvider) and T.TryParse(string, IFormatProvider, out TSelf).
ISpanParsable<TSelf> Abstracts T.Parse(ReadOnlySpan<char>, IFormatProvider) and T.TryParse(ReadOnlySpan<char>, IFormatProvider, out TSelf).
IFormattable Abstracts value.ToString(string, IFormatProvider).
ISpanFormattable Abstracts value.TryFormat(Span<char>, out int, ReadOnlySpan<char>, IFormatProvider).

Interface Implementation and Boxing

Since the introduction of Generic Math, primitive types such as int and double which are structures, now implement some interfaces. But wait, isn’t it generally bad for a structure to implement an interface? This typically leads to boxing. Boxing occurs when the value of a structure is cast to a reference type, such as System.Object or an interface. To accommodate this, an object must be created to host the value. This additional object puts pressure on the Garbage Collector, which is why boxing negatively impacts performance. The program below shows how casting a structure leads to an IL box instruction to be emitted by the C# compiler:

struct and boxing

Keep in mind that Generic Math is designed to abstract algorithms from number representations. As a result, interfaces in Generic Math are typically used within generic methods and generic types. Fortunately, .NET generics have been carefully designed since their inception to avoid boxing in this context. This is demonstrated in the program below that doesn’t rely on the box IL instruction (the method DisposeIt<T>() also does not contain any box IL instruction):

struct and generic

Conclusion

Generic Math is a remarkable innovation introduced in .NET. It involved deep modifications to core primitive types such as int and double, while ensuring that these changes did not negatively impact the performance of existing scenarios. It was made possible thanks to:

  • the implementation of generics in .NET that was meticulously thought out from its very beginning with C# 2
  • the interface static abstract members that were added to C# 11 and the runtime. They are explained in detail in this blog post.

Initially, Generic Math was an experiment from Miguel de Icaza through the project PartyDonk.

 

 

Comments:

  1. Mark Whybird says:

    Very interesting.

    Minor detail that could be just a blog formatting issue: In the table under “Parsing and formatting interfaces”, IFormattable and ISpanFormattable both have a superscript 1 after their names, like as if there is some relevant footnote, but I couldn’t see any footnote. Did I misinterpret that?

  2. Mark Whybird says:

    Thanks

  3. “and dividing two integers returns a double: 5 / 2 = 2.5” no it does not

    ❯ dotnet script
    > 5 / 2
    2

  4. Thanks, Steve, we updated the example by multiplying 2 byte to return an int

Comments are closed.