Repository navigation
[API Proposal]: Add Decimal32, Decimal64, and Decimal128 from the IEEE 754-2019 standard. #81376
Description
Background and motivation
This is a restructuring of the original API proposal here: #69777
The existing System.Decimal type does not conform to the IEEE standard for decimal floating-point types. We have no plans to rehash System.Decimal, but adding Decimal32, Decimal64, and Decimal128 in addition would allow users to work within a standard that is being adopted by other languages and frameworks. There is also a future where hardware support for these types is more widely adopted, and having IEEE-conforming types will allow us to users to take advantage of performance gains.
For reference, here is a chart comparing the existing System.Decimal type to the IEEE types:
| Decimal Comparison Chart | System.Decimal | Decimal128 | Decimal64 | Decimal32 |
|---|---|---|---|---|
| Size | 128 bits | 128 bits | 64 bits | 32 bits |
| Max value | ~7.9e28 | ~9.9e6144 | ~9.9e384 | ~9.9e96 |
| Min value | ~-7.9e28 | ~-9.9e6144 | ~-9.9e384 | ~-9.9e96 |
| Smallest magnitude non-zero value | 1e-28 | 1e-6176 | 1e-398 | 1e-101 |
| Decimal digits of precision | 28 | 34 | 16 | 7 |
Alternative Designs
- Notably, a few APIs below have been annotated with
PLATINUM. These APIs represent high-implementation-cost functionality that is only "recommended" by IEEE. These are APIs that we might want eventually, as implementing all of them will allow these types to inherit fromIFloatingPointIeee754instead of justIFloatingPoint. Given the implementation cost, we are targeting shipping a smaller surface for .NET 8 that does not include thePLATINUMAPIs. - While this proposal covers all three types, there is an argument for focusing on one of them for .NET 8. The implementation cost of adding all three isn't exactly 3x the cost of adding one, but it isn't completely trivial either.
- Do we want a constructor? Decimal128 can sometimes use the
.123mliteral syntax (because it represents a superset of System.Decimal), but otherwise the only real way to accurately create these types is via parsing. EDIT: yes, added constructor
Risks/Considerations
- There is a potential risk when it comes to code size, especially for
Decimal128, as the current lack of hardware support will require the below APIs to be implemented in software. - These must be implemented such that it is easy to wire up hardware support if/when it becomes more widespread.
- There is potential for confusion with these coexisting with
System.Decimal. We are mitigating this by placing these types inSystem.Numerics. Clear documentation will be required.
API Proposal
Decimal32 is shown, but the API surfaces for Decimal64 and Decimal128 are nearly identical, bar some differences in the conversions (which are noted below). This proposal is for all three types.
namespace System.Numerics
{
/// <summary>Defines an IEEE 754 floating-point type that is represented in a base-10 format.</summary>
/// <typeparam name="TSelf">The type that implements the interface.</typeparam>
public interface IDecimalFloatingPointIeee754<TSelf> // PLATINUM
: IFloatingPointIeee754<TSelf>
where TSelf : IDecimalFloatingPointIeee754<TSelf>
{
// IEEE Spec 5.3.2
static abstract TSelf Quantize(TSelf x, TSelf y);
static abstract TSelf Quantum(TSelf x);
// IEEE Spec 5.7.3
static abstract bool SameQuantum(TSelf x, TSelf y);
}
//
// Summary:
// Represents a 32-bit IEEE decimal floating-point number
[StructLayout(LayoutKind.Sequential)]
public readonly struct Decimal32
: IComparable<Decimal32>,
IComparable,
ISpanFormattable,
ISpanParsable<Decimal32>,
IEquatable<Decimal32>,
IFloatingPoint<Decimal32>,/*PLATINUM: Replace with IDecimalFloatingPointIeee754<Decimal32>,*/
IMinMaxValue<Decimal32>
{
internal readonly uint _value; // NOTE: this is a ulong for Decimal64, and a UInt128 for Decimal128
public Decimal32(int significand, int exponent); // NOTE: params are (long, int) for Decimal64 and (Int128, int) for Decimal128
//
// Parsing (INumberBase, IParsable, ISpanParsable, other)
//
public static Decimal32 Parse(string s);
public static Decimal32 Parse(string s, NumberStyles style);
public static Decimal32 Parse(ReadOnlySpan<char> s, IFormatProvider? provider);
public static Decimal32 Parse(string s, IFormatProvider? provider);
public static Decimal32 Parse(ReadOnlySpan<char> s, NumberStyles style = DefaultParseStyle, IFormatProvider? provider = null);
public static Decimal32 Parse(string s, NumberStyles style, IFormatProvider? provider);
public static bool TryParse([NotNullWhen(true)] string? s, out Decimal32 result);
public static bool TryParse(ReadOnlySpan<char> s, out Decimal32 result);
public static bool TryParse(ReadOnlySpan<char> s, IFormatProvider? provider, [MaybeNullWhen(false)] out Decimal32 result);
public static bool TryParse([NotNullWhen(true)] string? s, IFormatProvider? provider, [MaybeNullWhen(false)] out Decimal32 result);
public static bool TryParse(ReadOnlySpan<char> s, NumberStyles style, IFormatProvider? provider, [MaybeNullWhen(false)] out Decimal32 result);
public static bool TryParse([NotNullWhen(true)] string? s, NumberStyles style, IFormatProvider? provider, [MaybeNullWhen(false)] out Decimal32 result);
//
// Misc. Methods (including IComparable, IEquatable, other)
//
public int CompareTo(object? obj);
public int CompareTo(Decimal32 other);
public override bool Equals([NotNullWhen(true)] object? obj);
public bool Equals(Decimal32 other);
public override int GetHashCode();
// 5.5.2 of the IEEE Spec
public static uint EncodeDecimal(Decimal32 x); // NOTE: this is a ulong for Decimal64, and a UInt128 for Decimal128
public static Decimal32 DecodeDecimal(uint x); // NOTE: this is a ulong for Decimal64, and a UInt128 for Decimal128
public static uint EncodeBinary(Decimal32 x); // NOTE: this is a ulong for Decimal64, and a UInt128 for Decimal128
public static Decimal32 DecodeBinary(uint x); // NOTE: this is a ulong for Decimal64, and a UInt128 for Decimal128
//
// Formatting (IFormattable, ISpanFormattable, other)
//
public override string ToString();
public string ToString([StringSyntax(StringSyntaxAttribute.NumericFormat)] string? format);
public string ToString(IFormatProvider? provider);
public string ToString([StringSyntax(StringSyntaxAttribute.NumericFormat)] string? format, IFormatProvider? provider);
public bool TryFormat(Span<char> destination, out int charsWritten, [StringSyntax(StringSyntaxAttribute.NumericFormat)] ReadOnlySpan<char> format = default, IFormatProvider? provider = null);
//
// Explicit Convert To Decimal32
// (T -> Decimal32 is lossy)
//
public static explicit operator Decimal32(int value); // NOTE: Decimal64 and Decimal128 will have this as *implicit*
public static explicit operator Decimal32(uint value); // NOTE: Decimal64 and Decimal128 will have this as *implicit*
public static explicit operator Decimal32(nint value); // NOTE: Decimal128 will have this as *implicit*
public static explicit operator Decimal32(nuint value); // NOTE: Decimal128 will have this as *implicit*
public static explicit operator Decimal32(long value); // NOTE: Decimal128 will have this as *implicit*
public static explicit operator Decimal32(ulong value); // NOTE: Decimal128 will have this as *implicit*
public static explicit operator Decimal32(Int128 value);
public static explicit operator Decimal32(UInt128 value);
public static explicit operator Decimal32(Half value);
public static explicit operator Decimal32(float value);
public static explicit operator Decimal32(double value);
public static explicit operator Decimal32(decimal value); // NOTE: Decimal128 will have this as *implicit*
public static explicit operator Decimal32(Decimal64 value);
public static explicit operator Decimal32(Decimal128 value);
//
// Explicit Convert From Decimal32
// (Decimal32 -> T is lossy)
// - Includes a "checked" conversion if T cannot represent infinity and NaN
//
public static explicit operator byte(Decimal32 value);
public static explicit operator checked byte(Decimal32 value);
public static explicit operator sbyte(Decimal32 value);
public static explicit operator checked sbyte(Decimal32 value);
public static explicit operator char(Decimal32 value);
public static explicit operator checked char(Decimal32 value);
public static explicit operator short(Decimal32 value);
public static explicit operator checked short(Decimal32 value);
public static explicit operator ushort(Decimal32 value);
public static explicit operator checked ushort(Decimal32 value);
public static explicit operator int(Decimal32 value);
public static explicit operator checked int(Decimal32 value);
public static explicit operator uint(Decimal32 value);
public static explicit operator checked uint(Decimal32 value);
public static explicit operator nint(Decimal32 value);
public static explicit operator checked nint(Decimal32 value);
public static explicit operator nuint(Decimal32 value);
public static explicit operator checked nuint(Decimal32 value);
public static explicit operator long(Decimal32 value);
public static explicit operator checked long(Decimal32 value);
public static explicit operator ulong(Decimal32 value);
public static explicit operator checked ulong(Decimal32 value);
public static explicit operator Int128(Decimal32 value);
public static explicit operator checked Int128(Decimal32 value);
public static explicit operator UInt128(Decimal32 value);
public static explicit operator checked UInt128(Decimal32 value);
public static explicit operator Half(Decimal32 value);
public static explicit operator float(Decimal32 value);
public static explicit operator double(Decimal32 value);
public static explicit operator decimal(Decimal32 value); // Doesn't have a "checked" for historical reasons
//
// Implicit Convert To Decimal32
// (T -> Decimal32 is not lossy)
//
public static implicit operator Decimal32(byte value);
public static implicit operator Decimal32(sbyte value);
public static implicit operator Decimal32(char value);
public static implicit operator Decimal32(short value);
public static implicit operator Decimal32(ushort value);
//
// Implicit Convert From Decimal32
// (Decimal32 -> T is not lossy)
//
public static implicit operator Decimal64(Decimal32 value);
public static implicit operator Decimal128(Decimal32 value);
//
// IAdditionOperators
//
public static Decimal32 operator +(Decimal32 left, Decimal32 right);
//
// IAdditiveIdentity
//
static Decimal32 IAdditiveIdentity<Decimal32, Decimal32>.AdditiveIdentity { get; }
//
// IComparisonOperators
//
public static bool operator <(Decimal32 left, Decimal32 right);
public static bool operator >(Decimal32 left, Decimal32 right);
public static bool operator <=(Decimal32 left, Decimal32 right);
public static bool operator >=(Decimal32 left, Decimal32 right);
//
// IDecimalFloatingPointIeee754
//
public static Decimal32 Quantize(Decimal32 x, Decimal32 y);
public static Decimal32 Quantum(Decimal32 x);
public static bool SameQuantum(Decimal32 x, Decimal32 y);
//
// IDecrementOperators
//
public static Decimal32 operator --(Decimal32 value);
//
// IDivisionOperators
//
public static Decimal32 operator /(Decimal32 left, Decimal32 right);
//
// IEqualityOperators
//
public static bool operator ==(Decimal32 left, Decimal32 right);
public static bool operator !=(Decimal32 left, Decimal32 right);
//
// IExponentialFunctions
//
public static Decimal32 Exp(Decimal32 x); // PLATINUM
public static Decimal32 ExpM1(Decimal32 x); // PLATINUM
public static Decimal32 Exp2(Decimal32 x); // PLATINUM
public static Decimal32 Exp2M1(Decimal32 x); // PLATINUM
public static Decimal32 Exp10(Decimal32 x); // PLATINUM
public static Decimal32 Exp10M1(Decimal32 x); // PLATINUM
//
// IFloatingPoint
//
public static Decimal32 Ceiling(Decimal32 x);
public static Decimal32 Floor(Decimal32 x);
public static Decimal32 Round(Decimal32 x);
public static Decimal32 Round(Decimal32 x, int digits);
public static Decimal32 Round(Decimal32 x, MidpointRounding mode);
public static Decimal32 Round(Decimal32 x, int digits, MidpointRounding mode);
public static Decimal32 Truncate(Decimal32 x);
int IFloatingPoint<Decimal32>.GetExponentByteCount();
int IFloatingPoint<Decimal32>.GetExponentShortestBitLength();
int IFloatingPoint<Decimal32>.GetSignificandBitLength();
int IFloatingPoint<Decimal32>.GetSignificandByteCount();
bool IFloatingPoint<Decimal32>.TryWriteExponentBigEndian(Span<byte> destination, out int bytesWritten);
bool IFloatingPoint<Decimal32>.TryWriteExponentLittleEndian(Span<byte> destination, out int bytesWritten);
bool IFloatingPoint<Decimal32>.TryWriteSignificandBigEndian(Span<byte> destination, out int bytesWritten);
bool IFloatingPoint<Decimal32>.TryWriteSignificandLittleEndian(Span<byte> destination, out int bytesWritten);
//
// IFloatingPointConstants
//
public static Decimal32 E { get; }
public static Decimal32 Pi { get; }
public static Decimal32 Tau { get; }
//
// IFloatingPointIeee754
//
public static Decimal32 Epsilon { get; }
public static Decimal32 NaN { get; }
public static Decimal32 NegativeInfinity { get; }
public static Decimal32 NegativeZero { get; }
public static Decimal32 PositiveInfinity { get; }
public static Decimal32 Atan2(Decimal32 y, Decimal32 x); // PLATINUM
public static Decimal32 Atan2Pi(Decimal32 y, Decimal32 x); // PLATINUM
public static Decimal32 BitDecrement(Decimal32 x);
public static Decimal32 BitIncrement(Decimal32 x);
public static Decimal32 FusedMultiplyAdd(Decimal32 left, Decimal32 right, Decimal32 addend);
public static Decimal32 Ieee754Remainder(Decimal32 left, Decimal32 right);
public static int ILogB(Decimal32 x);
public static Decimal32 Lerp(Decimal32 value1, Decimal32 value2, Decimal32 amount);
public static Decimal32 ReciprocalEstimate(Decimal32 x);
public static Decimal32 ReciprocalSqrtEstimate(Decimal32 x);
public static Decimal32 ScaleB(Decimal32 x, int n);
// public static Decimal32 Compound(Half x, Decimal32 n); (Already approved in API review but not implemented yet) // PLATINUM
//
// IHyperbolicFunctions
//
public static Decimal32 Acosh(Decimal32 x); // PLATINUM
public static Decimal32 Asinh(Decimal32 x); // PLATINUM
public static Decimal32 Atanh(Decimal32 x); // PLATINUM
public static Decimal32 Cosh(Decimal32 x); // PLATINUM
public static Decimal32 Sinh(Decimal32 x); // PLATINUM
public static Decimal32 Tanh(Decimal32 x); // PLATINUM
//
// IIncrementOperators
//
public static Decimal32 operator ++(Decimal32 value);
//
// ILogarithmicFunctions
//
public static Decimal32 Log(Decimal32 x); // PLATINUM
public static Decimal32 Log(Decimal32 x, Decimal32 newBase); // PLATINUM
public static Decimal32 Log10(Decimal32 x); // PLATINUM
public static Decimal32 LogP1(Decimal32 x); // PLATINUM
public static Decimal32 Log2(Decimal32 x); // PLATINUM
public static Decimal32 Log2P1(Decimal32 x); // PLATINUM
public static Decimal32 Log10P1(Decimal32 x); // PLATINUM
//
// IMinMaxValue
//
public static Decimal32 MaxValue { get; }
public static Decimal32 MinValue { get; }
//
// IModulusOperators
//
public static Decimal32 operator %(Decimal32 left, Decimal32 right);
//
// IMultiplicativeIdentity
//
public static Decimal32 MultiplicativeIdentity { get; }
//
// IMultiplyOperators
//
public static Decimal32 operator *(Decimal32 left, Decimal32 right);
//
// INumber
//
public static Decimal32 Clamp(Decimal32 value, Decimal32 min, Decimal32 max);
public static Decimal32 CopySign(Decimal32 value, Decimal32 sign);
public static Decimal32 Max(Decimal32 x, Decimal32 y);
public static Decimal32 MaxNumber(Decimal32 x, Decimal32 y);
public static Decimal32 Min(Decimal32 x, Decimal32 y);
public static Decimal32 MinNumber(Decimal32 x, Decimal32 y);
public static int Sign(Decimal32 value);
//
// INumberBase (well defined/commonly used values)
//
public static Decimal32 One { get; }
static int INumberBase<Decimal32>.Radix; // Note: this ideally should be exposed implicitly as it is required by IEEE
public static Decimal32 Zero { get; }
public static Decimal32 Abs(Decimal32 value);
public static Decimal32 CreateChecked<TOther>(TOther value);
public static Decimal32 CreateSaturating<TOther>(TOther value);
public static Decimal32 CreateTruncating<TOther>(TOther value);
static bool INumberBase<Decimal32>.IsCanonical(Decimal32 value); // Note: this ideally should be exposed implicitly as it is required by IEEE
static bool INumberBase<Decimal32>.IsComplexNumber(Decimal32 value);
public static bool IsEvenInteger(Decimal32 value);
public static bool IsFinite(Decimal32 value);
static bool INumberBase<Decimal32>.IsImaginaryNumber(Decimal32 value);
public static bool IsInfinity(Decimal32 value);
public static bool IsInteger(Decimal32 value);
public static bool IsNaN(Decimal32 value);
public static bool IsNegative(Decimal32 value);
public static bool IsNegativeInfinity(Decimal32 value);
public static bool IsNormal(Decimal32 value);
public static bool IsOddInteger(Decimal32 value);
public static bool IsPositive(Decimal32 value);
public static bool IsPositiveInfinity(Decimal32 value);
public static bool IsRealNumber(Decimal32 value);
public static bool IsSubnormal(Decimal32 value);
static bool INumberBase<Decimal32>.IsZero(Decimal32 value); // Note: this ideally should be exposed implicitly as it is required by IEEE
public static Decimal32 MaxMagnitude(Decimal32 x, Decimal32 y);
public static Decimal32 MaxMagnitudeNumber(Decimal32 x, Decimal32 y);
public static Decimal32 MinMagnitude(Decimal32 x, Decimal32 y);
public static Decimal32 MinMagnitudeNumber(Decimal32 x, Decimal32 y);
static bool INumberBase<Decimal32>.TryConvertFromChecked<TOther>(TOther value, out Decimal32 result);
static bool INumberBase<Decimal32>.TryConvertFromSaturating<TOther>(TOther value, out Decimal32 result);
static bool INumberBase<Decimal32>.TryConvertFromTruncating<TOther>(TOther value, out Decimal32 result);
static bool INumberBase<Decimal32>.TryConvertToChecked<TOther>(Decimal32 value, [MaybeNullWhen(false)] out TOther result);
static bool INumberBase<Decimal32>.TryConvertToSaturating<TOther>(Decimal32 value, [MaybeNullWhen(false)] out TOther result);
static bool INumberBase<Decimal32>.TryConvertToTruncating<TOther>(Decimal32 value, [MaybeNullWhen(false)] out TOther result);
//
// IPowerFunctions
//
public static Decimal32 Pow(Decimal32 x, Decimal32 y); // PLATINUM
//
// IRootFunctions
//
public static Decimal32 Cbrt(Decimal32 x); // PLATINUM
public static Decimal32 Hypot(Decimal32 x, Decimal32 y); // PLATINUM
public static Decimal32 RootN(Decimal32 x, int n); // PLATINUM
public static Decimal32 Sqrt(Decimal32 x);
//
// ISignedNumber
//
public static Decimal32 NegativeOne { get; }
//
// ISubtractionOperators
//
public static Decimal32 operator -(Decimal32 left, Decimal32 right);
//
// ITrigonometricFunctions
//
public static Decimal32 Acos(Decimal32 x); // PLATINUM
public static Decimal32 AcosPi(Decimal32 x); // PLATINUM
public static Decimal32 Asin(Decimal32 x); // PLATINUM
public static Decimal32 AsinPi(Decimal32 x); // PLATINUM
public static Decimal32 Atan(Decimal32 x); // PLATINUM
public static Decimal32 AtanPi(Decimal32 x); // PLATINUM
public static Decimal32 Cos(Decimal32 x); // PLATINUM
public static Decimal32 CosPi(Decimal32 x); // PLATINUM
public static Decimal32 Sin(Decimal32 x); // PLATINUM
public static (Decimal32 Sin, Decimal32 Cos) SinCos(Decimal32 x); // PLATINUM
public static (Decimal32 SinPi, Decimal32 CosPi) SinCosPi(Decimal32 x); // PLATINUM
public static Decimal32 SinPi(Decimal32 x); // PLATINUM
public static Decimal32 Tan(Decimal32 x); // PLATINUM
public static Decimal32 TanPi(Decimal32 x); // PLATINUM
//
// IUnaryNegationOperators
//
public static Decimal32 operator -(Decimal32 value);
//
// IUnaryPlusOperators
//
public static Decimal32 operator +(Decimal32 value);
}
}
Activity
AaronRobinsonMSFT commentedon Jan 30, 2023
This is a bit strong. In order for customers to benefit from future hardware these APIs must be implemented. The above implies these APIs should be implemented because of future hardware which doesn't seem correct. This is an increasingly niche API surface area and I think we should determine a customer and if it is a current need for .NET 8 or perhaps a future release.
dakersnar commentedon Jan 30, 2023
@AaronRobinsonMSFT Let me adjust my wording, what I meant to imply with that bullet point was that if we are going to implement these types, we must implement them with future hardware support in mind. In other words, we should implement them in such a way that, if hardware support is eventually released and widespread, we can easily wire these up to those intrinsics.
tannergooding commentedon Jan 30, 2023
@AaronRobinsonMSFT, I think you're misunderstanding the statement here.
The statement is one around ensuring we consider that there are two backing encodings for decimal (one oriented towards software and another towards hardware) and that we should ensure our API surface isn't forcing one or the other.
Such hardware supporting the IEEE 754 decimal types already exists and has been in production use at the enterprise level for years (namely IBM PowerPC).
I likewise think this is a misunderstanding. We have had
decimalsince .NET v1.0 and since its introduction developers having been asking for extended functionality that cannot be provided by that type (namely because it has a strict 1-to-1 requirement with the underlying DECIMAL/CY type in Win32).The IEEE 754 decimal types are an industry standard that has already been proven to be successful, which has been an ABI standard for over 12 years, and which is actively getting first class support in a number of other languages.
This points towards it being a viable answer towards both the perf and extension points customers have already been asking for around
System.Decimalwhich we've not been able to provide.AaronRobinsonMSFT commentedon Jan 30, 2023
Gotcha. That makes sense. @dakersnar's statement also helped to clarify that.
KTSnowy commentedon Jan 31, 2023
Copying my comment from #79004 and adding a little more context.
I'm the current lead developer of Otterkit, a free and open source COBOL compiler for dotnet (Implementing the COBOL 2022 standard). We're looking forward to being able to use these types in our compiler.
We are currently PInvoking calls to the mpdecimal library to provide this functionality on dotnet, but that makes the build process much more complicated and hurts portability. Having these types available directly on C# would be awesome for our project.
The reason for this is that COBOL relies heavily on decimal arithmetic, the COBOL standard requires support for the Decimal128 type for its standard-decimal arithmetic mode, and requires a decimal implementation with at least 31 digits of precision for its native arithmetic mode. The decimal type we have in C# right now is not compatible with these requirements.
Our COBOL runtime library only requires the Decimal128 type, both the Decimal32 and Decimal64 use a truncated Decimal128. This works quite well because all of these types' max/min values only contains 9s and an exponent (
+-99999...E+-...). There doesn't seem to be a downside to this as long as the Decimal128 implementation is performant enough, and the truncation and overflow checks matches the IEEE requirements for those types' max/min values. The final value is then stored in Decimal32 and Decimal64 formats, but the math itself is done as if it was a Decimal128.Hi @dakersnar, COBOL provides some of those math functions with its intrinsic functions, and implementing those is required by the COBOL standard. The mpdecimal library that we're using unfortunately does not completely provide that functionality so we had to implement it ourselves in the runtime library (mpdecimal doesn't provide trigonometric functions).
Because of the COBOL standard's requirements we would require support for the
IExponentialFunctions,ILogarithmicFunctions,IPowerFunctions,IRootFunctions,ITrigonometricFunctions.COBOL itself does not provide support for anything from
IHyperbolicFunctionsso we would not require support for it.Having those directly in C# would be awesome, we would be able to provide that COBOL functionality in our compiler in a way that is compatible with C#, and might make it easier to implement our COBOL <=> C# Bridge in the future.
I'm not sure if I'm allowed to request any extra functionality for this, but if possible, would there be a way to support UTF-8 encoded
ToSpanandFromSpanmethods for these types? Otterkit heavily depends onSpan<byte>andMemory<byte>for every COBOL data type, every type is implemented using UTF-8 encoded Spans. UsingSpan<char>would not work due to COBOL's very specific picture formatting rules.richlander commentedon Jan 31, 2023
Please consider yourself "allowed". I'm sure the team would appreciate knowing more about your needs, particularly to the end of influencing their design and prioritization.
dakersnar commentedon Jan 31, 2023
@KTSnowy just so I can fully understand your scenario, let me outline a few possibilities:
At which level does it become feasible for you to plug our types into your project, if only to test the ease of integration and measure performance? Following that, at which level does it become beneficial to plug the types into your project? Obviously, it sounds like level 3 is what you would most prefer. In the event level 3 is not feasible for this release cycle, is there a "half measure" for .NET 8 that you would still be able to use?
Edit: Additionally, I want to note that .NET does not currently support some aspects of the IEEE spec including global rounding modes, floating-point exceptions, and flag setting. Does your project require these features?
64 remaining items
migueldecimal128 commentedon Jul 29, 2026
Here is an updated review of System.Numerics.Decimal using the daily build of 2026-07-26
https://migueldecimal128.github.io/decimal128-www/reviews/dotnet/net11-preview7-26376.106.html
This includes many of the things that you flagged as "already done" when I did my initial review a few days ago against the Jul 14 build.
I agree that decoding the combination field is expensive in software ... in hardware it would be straightforward.
Using either of the DPD/BID interchange formats as an in-memory representation is sub-optimal ... they were designed for hardware, not software implementations. Intel had different objectives when they wrote the libbid library ... they had to use BID. I can demonstrate that one can unpack the combination-field and significantly reduce the run-time decoding cost while staying in 128 bits.
For your initial release I encourage you to make sure that your API does not expose your BID in-memory representation ... force users to use Encode/DecodeBinary/Decimal when performing IO. This will reserve the option of changing your in-memory representation for Decimal128 in the future ... and getting significantly better performance for fastpath operations ... should you choose to do so.
I agree completely. Decimal128 offers a lot more functionality than System.Decimal ... no comparison. I included System.Decimal (where applicable) in the benchmark tables simply because I thought it would be interesting.
This is a topic for discussion after you folks have shipped dotnet11.
tannergooding commentedon Jul 29, 2026
This is not really accurate. Both were designed for either, with BID notably being explicitly oriented around either software or hardware acceleration.
The in memory representation can never change for x64 or Arm64. As I've already covered, it being BID is an explicit requirement of the underlying system ABI.
It is already allowed to be DPD on some other theoretical platform that defines that for its ABI, but RyuJIT has no such targets today.
The callout was not because
System.Decimalwas inappropriate to include, its that theDecimal128perf numbers you provided are impossible to achieve without some form of constant folding occurring.migueldecimal128 commentedon Jul 29, 2026
I suggest that locking your ABI into BID not only complicates/precludes DPD in hardware, but locks you in to a permanent performance cap on software implementations.
The benchmark numbers speak for themselves ... a software implementation in C#/RyuJIT can significantly outperform libbid in C.
https://migueldecimal128.github.io/decimal128-www/reviews/dotnet/net11-preview7-26376.106.html
With a different in-memory representation one can eliminate the (expensive) decoding of the combination field during operations.
https://migueldecimal128.github.io/decimal128-www/whitepapers/ubd.html
tannergooding commentedon Jul 29, 2026
I'll repeat one more time that x64 and Arm64 are already locked into requiring BID in perpetuity because the underlying native ABI for those platforms requires it. Because those ABIs have some level of oversight from the hardware vendors, those chipsets will likely never ship DPD support. This is not .NET is restricting things here, this is the platforms themselves have already added a fundamental requirement for it to be BID and correspondingly .NET matches that.
Likewise, .NET is already not precluded from using DPD. That can and would be supported on a platform that requires it for their ABI. It would then likewise be locked in to being DPD on those platforms.
This is strictly not an IEEE 754 Decimal type at that point. The in-memory representation is the most fundamental part of the spec and interchange for the types. Not following the documented encoding and additionally not following the platforms required ABI is a non-starter and makes any implementation unusable for its core purpose as an interchange type.
migueldecimal128 commentedon Jul 29, 2026
Pls educate me by providing reference to such an ABI.
I suggest that there is another way to read the spec.
The focus of the spec is about behavior and functionality for consistent cross-platform operations.
Bit representation for interchange is a small part of the spec.
IEEE 754-2019:
tannergooding commentedon Jul 29, 2026
This is the official ABI for Linux x64: https://gitlab.com/x86-psABIs/x86-64-ABI, note in particular:
Here is the same section for Arm64, which is essentially shared across all platforms: https://github.com/ARM-software/abi-aa/blob/main/aapcs64/aapcs64.rst#54decimal-floating-point
I would strongly disagree. Interchange is one of the most core factors of any reusable type and is central to it succeeding broadly in an ecosystem, particularly for things which are designated as "primitives". A type has no need to exist as part of the core libraries otherwise, it could in fact be provided by any random package as its own custom user-defined type.
There is likewise no need to follow IEEE 754 Decimal at all without the interchange aspect as well. An implementation could freely design a custom type that has the same general support, without all the negative historical baggage (particularly the BID/DPD split which is the primary reason for perf loss, BID itself without having to decode the combination field to differentiate it from DPD is superior for both software and dedicated hardware to handle).
masonwheeler commentedon Jul 29, 2026
@migueldecimal128 You and Tanner seem to be talking past each other here. You're talking specifically about the spec as a self-contained matter, whereas Tanner keeps circling back around to the concept of "interchange type." .NET is using this Decimal128 implementation not only as its own thing simply within .NET, but also as a type that can be passed to other non-.NET code, the same way you might pass an int or a float to an external function. Because of this, .NET has to represent these types in-memory in the way that non-.NET code will expect, as defined by existing ABI conventions that are out of the .NET team's hands.
Start with "we need to be able to share this with external code" as a core, non-negotiable requirement, and the rest of the .NET team's position makes a lot more sense.
migueldecimal128 commentedon Jul 29, 2026
I now fully understand that BID is an externally-imposed constraint because of binary interoperability with external systems. I will not raise the in-memory-representation issue again in the context of .NET
masonwheeler commentedon Jul 29, 2026
@tannergooding Having said that, it should be entirely uncontroversial to note that internal usage of a data type — any basic type — is expected to outweigh interchange usage by multiple orders of magnitude. If Miguel is correct in his claims of the significant performance benefits afforded by the DPD encoding, might it be worth at least looking into using DPD internally and marshalling to BID at ABI boundaries? It's not like we don't already do that with strings, so the basic plumbing is already there.
MichalPetryka commentedon Jul 29, 2026
Most value types are defined to be blittable so this would likely be done. If conversions would be cheap though, APIs could internally convert into custom format and then back if it'd be faster.
tannergooding commentedon Jul 29, 2026
Basic types form a foundational interchange layer, including interchange within a single language or runtime.
Decimal32/64/128are ABI primitives and so the managed layout, even without bringing another language or ecosystem into play, is expected and required to match the underlying native ABI here. Just as wouldInt32,Int64,Single,Double,Half, etc.Deviating at all is a non-starter. It is part of the guaranteed contract that on x64 and Arm64, you can safely blit and rely on this if necessary or appropriate.
DPD itself is not significantly different from BID, other than how it encodes the significand. Rather than encoding it as a single base 2-integral value, it instead encodes groups of 3 digits (
[000, 999]) together in 10 bits; You then have 11 of these groupings plus an additional leading digit of[0, 9]forDecimal128. To handle this, you have to do the operation and consider crossing/rounding across each of these 11 lanes, so its essentially not feasible to handle it in software.So the benefits here are not really from DPD at all, DPD is fundamentally more expensive for software to support, even when you get into some of the advanced tricks. Because for
BID, its just a regular 112-bit binary integer. No different than you have for float/double and so reuses the same hardware, the same ALUs, the same accelerators and optimizations, etc. Hardware can build highly specialized support, and its conceptually simpler for an FPGA to support, but its still not foundationally faster than proper BID support. It is typically at best a wash, but then comes with the downsides of custom hardware being required rather than allowing sharing/reuse. -- This is also why almost every platform, aside from IBM, has opted for BID. It's better for software and the "optimal" approach for most hardware usage, should they decide to add it in the future (and now that C has also standardized it, that's entirely possible).The improvements, if any, come from avoiding the IEEE 754 encoding (both BID and DPD) altogether to minimize the decode/encode cost of the combination field. You effectively store this as
sign + unbiased-exponent + significand(rather thansign + biased exponent + trailing significandlikefloatmight do) and bypass the additional step altogether so its a simplehandle exponents, handle significand, done(which is effectively howSystem.Decimalworks, but with the caveat you need to handleNaN+Infinity).However, much of that benefit can be still be had by optimizing multi-step operations separately and essentially skipping
encode + decode, so you can keep the binary compat and get the perf benefit for everything except the most trivial one off cases. But that's something to look at in the future, not for .NET 11 (since we're locking down for RC1 already).migueldecimal128 commentedon Jul 30, 2026
My claim is that an alternate memory representation offers provably/demonstrably superior performance. (Note that this is definitely not DPD)
If marshaling could be done then that would give you the option to change in the future. String marshaling would seem to be the perfect parallel.
The decoding/encoding conversion is fast ... prob a few dozen instructions.
The problem is, if your base representation is BID, then you effectively pay for this conversion upon entry to every arithmetic operation ... instead of only at the API surface boundaries.
@tannergooding wrote:
I do not suggest that you should change your representation for .NET11.
I am suggesting that if you can refrain from leaking/exposing your internal representation thru your API (and ABI ... as I now understand) then you will retain the option to tweak your in-memory representation in the future, when you have had time and experience to evaluate/consider other options. Only you folks can decide whether avoiding the leak would require changes (a marshaling NOOP?) prior to .NET11 freeze.
tannergooding commentedon Jul 30, 2026
The in-memory representation will not change. It will forever be BID on x64 and Arm64 and that is something users can rely on. If we supported something like IBM system-Z, then it would forever be DPD on that platform (due to their ABI requirements). Users must be able to rely on this, even for purely managed code with no marshalling involved (and not getting into the topics around how built-in marshalling is not recommended).
What I was referring to is that compiler (JIT) optimizations can exist that ensure chains of arithmetic operations do not unnecessarily pay the cost of repeated decode/encode, which gives you the expected performance wins without violating the ABI requirements.
migueldecimal128 commentedon Aug 1, 2026
Regarding the full arithmetic surface for Decimal32 ...
Rather than implementing IDecimalFloatingPointIeee754, consider shipping Decimal32 only as a compact storage/interchange type, with conversion/promotion to/from Decimal64/128.
decimal64 and decimal128 are defined in IEEE 754 as basic arithmetic formats for performing arithmetic. decimal32 is not. decimal32 is defined only as an interchange format, intended for storage and transfer:
IBM Z & POWER hardware has no decimal32 arithmetic. They only offer conversion instructions to/from decimal64. This design decision was made over 2 decades ago.
This situation differs from Half/binary16 .... binary16 was also defined only as an interchange format, but it gained arithmetic support through GPU usage. Decimal32 has no comparable demand.
Generics make Decimal32 easy to implement, but the public API surface you choose to expose is permanent. You end up bloating the library and committing to support a format with no demonstrated demand.