Add Decimal32, Decimal64, Decimal128#100729
Conversation
|
Note regarding the |
| static int IDecimalIeee754UnpackInfo<Decimal128, Int128, UInt128>.NumberDigitsPrecision => NumberDigitsPrecision; | ||
|
|
||
|
|
||
| private static Int128[] Int128Powers10 => |
There was a problem hiding this comment.
If Int128 is not optimized, this can be optimally stored as a ReadOnlySpan of Int32/Int64 like Number.BigInteger.Pow10BigNumTable
| public override int GetHashCode() | ||
| { | ||
| return _value.GetHashCode(); | ||
| } |
There was a problem hiding this comment.
+0 and -0 should have the same hash code. The easiest way can be stripping the sign bit.
I also remember that there can be same values with different representations.
There was a problem hiding this comment.
@huoyaoyuan Thanks for your review. However this PR targets only the first phase as following: #81376 (comment) so I haven't added NegativeZero and PositiveZero. I will update GetHashCode method in the next phase.
|
This is on my radar to take a look at; just noting it might be a bit delayed due to other priorities for the .NET 9 release. CC. @jeffhandley |
|
Hi @jeffhandley , thanks for your review, I have resolved all of them and also added test case to bit-shifting comment. |
| internal Decimal128(UInt128 value) | ||
| { | ||
| _upper = value.Upper; | ||
| _lower = value.Lower; | ||
| } |
There was a problem hiding this comment.
nit: While this is convenient, it requires an extra step compared to just taking the upper/lower bits directly.
| private static UInt128 NegativeInfinityValue => new UInt128(upper: 0xf800_0000_0000_0000, lower: 0); | ||
| private static UInt128 ZeroValue => new UInt128(0, 0); | ||
| private static UInt128 NegativeZeroValue => new UInt128(0x8000_0000_0000_0000, 0); | ||
| private static UInt128 QuietNaNValue => new UInt128(0x7C00_0000_0000_0000, 0); |
There was a problem hiding this comment.
nit: The canonical qNaN value used on .NET tends to be the -qNaN as that is what xarch has historically produced. We should continue matching that semantic for the decimal types.
There was a problem hiding this comment.
Thanks, I have updated it.
| private const int MaxExponent = 6111; | ||
| private const int MinExponent = -6176; |
There was a problem hiding this comment.
Where do these come from?
emax should be 6144 and emin should therefore be -6143
There was a problem hiding this comment.
Looks like this might be tracking the stored values rather than the actual exponents of the underlying values (i.e. they are off by precision, 34, as they aren't taking into account the number of coefficient digits).
There likely needs to be a better name chosen and/or a clarifying comment that helps readers understand the nuance and correlate it to what the IEEE 754 spec details.
There was a problem hiding this comment.
Thanks, I have updated it to following the same with IEEE 754 specs.
| private const int MinExponent = -6176; | ||
| private const int Precision = 34; | ||
| private const int ExponentBias = 6176; | ||
| private const int NumberBitsExponent = 14; |
There was a problem hiding this comment.
This one has a bit of a confusing name. Unlike for binary based floating-point, there is no explicit exponent bitfield. Rather there is the sign bit, the combination field which is 17 bits, and the trailing significand field which is 110 bits.
The exponent is then extracted from the combination field based on the value of the first 2-5 leading bits, giving us a 14-bit value which never starts with 0b11....
There was a problem hiding this comment.
Hi @tannergooding according to IEEE 754 specs,
i) If G0 and G1 together are one of 00, 01, or 10, then the biased exponent E is formed from
G0 through Gw+1 and the significand is formed from bits Gw+2 through the end of the
encoding (including T).
ii) If G0 and G1 together are 11 and G2 and G3 together are one of 00, 01, or 10, then the
biased exponent E is formed from G2 through Gw+3 and the significand is formed by
prefixing the 4 bits (8 + Gw+4) to T.
Where k is a positive multiple of 32, for these encodings all of the following are true:
therefore, the number of bits for biased exponent in both cases is always (w + 1) bits so I calculated and treat it as 14. However, I have replaced it by two methods EncodeExponentToG0ThroughGwPlus1, EncodeExponentToG2ThroughGwPlus3 to make it familiar with original specs.
| _lower = value.Lower; | ||
| } | ||
|
|
||
| public Decimal128(Int128 significand, int exponent) |
There was a problem hiding this comment.
Still a pending comment here (https://github.com/dotnet/runtime/pull/100729/changes#r2027324363) on the constructor and how it shouldn't be public, etc.
| /// </summary> | ||
| /// <param name="s">The input to be parsed.</param> | ||
| /// <returns>The equivalent <see cref="Decimal128"/> value representing the input string. If the input exceeds Decimal128's range, a <see cref="PositiveInfinityValue"/> or <see cref="NegativeInfinityValue"/> is returned. </returns> | ||
| public static Decimal128 Parse(string s) => Parse(s, NumberStyles.Number, provider: null); |
There was a problem hiding this comment.
NumberStyles.Number doesn't seem right. This should likely be NumberStyles.Float much like the binary based formats, otherwise core scenarios like 1e6 won't work as expected.
There was a problem hiding this comment.
Thanks, I have replaced it by NumberStyles.Float | NumberStyles.AllowThousands.
| /// <returns><see langword="true" /> if the parse was successful, <see langword="false" /> otherwise.</returns> | ||
| public static bool TryParse([NotNullWhen(true)] string? s, NumberStyles style, IFormatProvider? provider, [MaybeNullWhen(false)] out Decimal128 result) | ||
| { | ||
| NumberFormatInfo.ValidateParseStyleFloatingPoint(style); |
There was a problem hiding this comment.
We recently enabled hex-based strings and so this either needs to temporarily skip that (with a tracking item to enable it) or ensure it is working
| /// </summary> | ||
| public override int GetHashCode() | ||
| { | ||
| return new UInt128(_upper, _lower).GetHashCode(); |
There was a problem hiding this comment.
| return new UInt128(_upper, _lower).GetHashCode(); | |
| return HashCode.Combine(_lower, _upper); |
There was a problem hiding this comment.
Well actually that's not quite right either. This needs to account for similar cases as the binary-based formats and so needs for NaN to produce the same hashcode as other NaN and for +0 and -0 to produce the same hash.
We may even want this to work the same as decimal where cases like 1 vs 1.0 vs 1.000 all produce the same hash code as well, that is they are effectively canonicalized prior to getting the hash.
| namespace System.Numerics | ||
| { | ||
| [StructLayout(LayoutKind.Sequential)] | ||
| public readonly struct Decimal32 |
There was a problem hiding this comment.
Generally the same comments from Decimal128 apply here. Same again for Decimal64
| return new DecodedDecimalIeee754<TValue>(signed, biasedExponent - TDecimal.ExponentBias, significand); | ||
| } | ||
|
|
||
| internal static int CompareDecimalIeee754<TDecimal, TValue>(TValue currentDecimalBits, TValue otherDecimalBits) |
There was a problem hiding this comment.
This deserves a comment covering that it follows .NET comparison semantics and so expects NaN.Equals(NaN) to be true and therefore fulfills the GetHashCode/Equals/CompareTo contract required for use in collection types and sorting.
| yield return new object[] { (-567.89).ToString(), defaultStyle, null, new Decimal128(-56789, -2) }; | ||
| yield return new object[] { "0.666666666666666666666666666666666650000000000000000000000000000000000000000000000000", defaultStyle, invariantFormat, new Decimal128(Int128.Parse(new string('6', 34)), -34) }; | ||
|
|
||
| yield return new object[] { "0." + new string('0', 6176) + "1", defaultStyle, invariantFormat, new Decimal128(0, 0) }; |
There was a problem hiding this comment.
There need to be tests for cases like 0.00 and 0e-5 which produce 0 with the expected exponent and sign. That is, Parse and ToString must preserve the appropriate number of trailing zero as expected for decimal types.
There was a problem hiding this comment.
Hi @tannergooding do we need to preserve the negative symbol because it is not included for the current decimal type.
decimal m = decimal.Parse("-0.00");
Console.WriteLine(m.ToString()); // output 0.00
JimmyCushnie
left a comment
There was a problem hiding this comment.
It looks like the new types are missing a bunch of interfaces they ought to have.
Here's what System.Numerics.Decimal128 implements as of this PR:
For comparison, here's all the interfaces that System.Decimal implements as of .NET 10:
runtime/src/libraries/System.Private.CoreLib/src/System/Decimal.cs
Lines 62 to 73 in 081d220
I'm not fully sure what all those other interfaces do, but at the very least, the new types ought to implement IFloatingPoint<TSelf>. I assume this was missed because this PR was created before IFloatingPoint<TSelf> existed.
(I am a noob, please excuse me if I'm missing something!)
|
@JimmyCushnie this first PR is just about exposing the base struct definitions for interchange. It doesn't include exposing the dozens of arithmetic operations and other functionality This is done to explicitly scope the size of the PR and allow incremental work. |
Resolve #81376