Skip to content

Conversation

@RaymondHuy
Copy link
Contributor

Resolve #81376

@ghost
Copy link

ghost commented Apr 6, 2024

Note regarding the new-api-needs-documentation label:

This serves as a reminder for when your PR is modifying a ref *.cs file and adding/modifying public APIs, please make sure the API implementation in the src *.cs file is documented with triple slash comments, so the PR reviewers can sign off that change.

@RaymondHuy RaymondHuy marked this pull request as draft April 6, 2024 17:29
@dotnet-policy-service dotnet-policy-service bot added the community-contribution Indicates that the PR has been added by a community member label Apr 6, 2024
static int IDecimalIeee754UnpackInfo<Decimal128, Int128, UInt128>.NumberDigitsPrecision => NumberDigitsPrecision;


private static Int128[] Int128Powers10 =>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If Int128 is not optimized, this can be optimally stored as a ReadOnlySpan of Int32/Int64 like Number.BigInteger.Pow10BigNumTable

Comment on lines 104 to 107
public override int GetHashCode()
{
return _value.GetHashCode();
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+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.

else
{
// Equivalant values are compared by their exponent parts,
// and the value with smaller exponent is considered closer to zero.
// This only applies to IEEE 754 decimals. Consider to add support if decimals are added into .NET.
return 0;
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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.

@tannergooding
Copy link
Member

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

@RaymondHuy RaymondHuy marked this pull request as ready for review April 9, 2024 17:21

if (number.DigitsCount + numberZeroDigits <= TDecimal.Precision)
{
byte* p = number.DigitsPtr + number.DigitsCount;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar case, we just want to use Span<byte> and the number.Digits.Slice(number.DigitsCount) instead

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(assuming its not rewritten to avoid using a string buffer entirely, which I think is possible and likely faster/better)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, I have updated it.

Comment on lines 95 to 97
public bool Signed { get; }
public int Exponent { get; }
public TSignificand Significand { get; }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This and other places could likely be a bit more explicit, such as specifying whether this is the biased or unbiased exponent and which part of the significand it is (such as if its already undergone the BID encoding transforms)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, I have updated the struct name and its property names to make it more readable

where TDecimal : unmanaged, IDecimalIeee754ParseAndFormatInfo<TDecimal, TValue>
where TValue : unmanaged, IBinaryInteger<TValue>
{
bool signed = (value & TDecimal.SignMask) != TValue.Zero;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like value is rather decimalBits, which would make it clearer it's not some arbitrary integer

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, I have updated it

TValue significand;
int exponent;

if ((value & TDecimal.G0G1Mask) == TDecimal.G0G1Mask)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given the binary string abcdmmmmefgt..., this is checking ab == 11 which indicates that we have exponent: cdmmmmef and significand: 100gt... instead of exponent: abcdmmmm and significand: 0efgt...

However, I don't see this handling abcd == 1111 which indicates it is an infinity or nan.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @tannergooding, this method is designed to encode the finite number case. I have updated the method name to reflect it. Infinite/rounding/normalize cases are handled outside of this method.
For NaN case, based on your suggestion(#81376 (comment)) I'm doing the first step, so I didn't include the NaN case handling in this PR. Just let me know if it should be included 😉

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NaN, negative zero, and infinity would all be considered part of the “basic handling” that allows this to be purely used as an interchange type.

The intent is to ensure that users can parse, format, and construct valid decimal values. Since those are all valid and core values, they need to be supported to ensure no behavioral breaking changes or bugs exist.

Effectively we want it to be a “minimal viable product” that could ship even if we didn’t have time to add support for all arithmetic operations

Comment on lines 152 to 155
if (current.Signed)
{
(current, other) = (other, current);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could just be handled in the earlier branch as the else case, to avoid checking the sign twice

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It can also be done prior to unpacking, to save on some perf

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, I have updated it

}
}

private static unsafe TValue NumberToDecimalIeee754Bits<TDecimal, TValue>(ref NumberBuffer number)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar case, we want to avoid unnecessary unsafe. The JIT should be able to do a generally good job with all the patterns we're using here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, I have updated it

}

significand = TDecimal.NumberToSignificand(ref number, number.DigitsCount);
return DecimalIeee754BinaryEncoding<TDecimal, TValue>(number.IsNegative, significand, exponent);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this return is duplicative of the return below

The function could also be refactored slightly to make it a bit shorter and simpler.

if ((number.DigitsCOunt <= TDecimal.Precision) || !DecimalIeee754Rounding<TDecimal, TValue>(ref number, TDecimal.Precision, out TValue significand, out int exponent))
{
    int positiveExponent = (Math.Max(0, number.Scale));
    int integerDigitsPresent = Math.Min(positiveExponent, number.DigitsCount);
    int fractionalDigitsPresent = number.DigitsCount - integerDigitsPresent;
    exponent = number.Scale - integerDigitsPresent - fractionalDigitsPresent;

    if (exponent < TDecimal.MinExponent)
    {
        return SmallExponentRounding(ref number, exponent);
    }

    significand = TDecimal.NumberToSignificand(ref number, number.DigitsCount);
}
else if (exponent > TDecimal.MaxExponent)
{
    return number.IsNegative ? TDecimal.NegativeInfinity : TDecimal.PositiveInfinity;
}

return DecimalIeee754BinaryEncoding<TDecimal, TValue>(number.IsNegative, significand, exponent);

Comment on lines 241 to 255
static TValue SmallExponentRounding(ref NumberBuffer number, int exponent)
{
Debug.Assert(exponent < TDecimal.MinExponent);
int numberDigitsRemove = (TDecimal.MinExponent - exponent);
if (numberDigitsRemove < number.DigitsCount)
{
int numberDigitsRemain = number.DigitsCount - numberDigitsRemove;
DecimalIeee754Rounding<TDecimal, TValue>(ref number, numberDigitsRemain, out TValue significand, out exponent);
return DecimalIeee754BinaryEncoding<TDecimal, TValue>(number.IsNegative, significand, exponent);
}
else
{
return TDecimal.Zero;
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure it's worth extracting this to a local method. It's likely to be inlined already plus results in a bit simpler code, with less asserts, if just declare it inline manually

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, I have updated it to be inlined.

Comment on lines +219 to +222
if (exponent > TDecimal.MaxExponent)
{
return number.IsNegative ? TDecimal.NegativeInfinity : TDecimal.PositiveInfinity;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This path likely deserves a comment (and maybe explicit test) how it handles TDecimal.MaxValue + (Delta / 2) vs TDecimal.MaxValue + ((Delta / 2) - 1)

i.e. float.MaxValue is 340282346638528859811704183484516925440 and the delta between representable values at that range is 20282409603651670423947251286016. So 340282346638528859811704183484516925440 + (20282409603651670423947251286016 / 2) is 340282356779733661637539395458142568448

You will then find that 340282356779733661637539395458142568448 rounds to +Infinity, while 340282356779733661637539395458142568447 rounds to float.MaxValue -- i.e. values larger than MaxValue can still round down if they are "nearer"

}
}

private static unsafe TValue DecimalIeee754BinaryEncoding<TDecimal, TValue>(bool signed, TValue significand, int exponent)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: exponent and significand are swapped compared to the typical order of such APIs

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These types of APIs, even though they are private/internal, likely also benefit from XML doc comments to be explicit that that significand is the "full" (non-trailing) significand and exponent is the unbiased exponent.

The terms get interchanged a lot and so being explicit can help ensure future readers understand the intent.

byte[] buffer = ArrayPool<byte>.Shared.Rent(TDecimal.BufferLength);
try
{
fixed (byte* pDigits = buffer)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

more cases that are using unnecessary unsafe. NumberBuffer takes a span

Comment on lines 347 to 348
char* stackPtr = stackalloc char[CharStackBufferSize];
var vlb = new ValueListBuilder<char>(new Span<char>(stackPtr, CharStackBufferSize));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this can directly use stackalloc in the ValueListBuilder constructor as well, since the language will convert it to span implicitly

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @tannergooding , I follow your instruction, but it throws this error,
image
do you have another suggestion, or we just keep the old one.

Copy link
Member

@tannergooding tannergooding Nov 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, unfortunately there's actually a missing language feature here due to how we pass the data to NumberToString. We need ref scoped SomeRefStruct to do this one in particular.

Other callouts may not need that feature, just this one in particular does.

Comment on lines 364 to 367
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't typically put the return in a finally block. An exception being thrown is very unexpected and returning is not required for correctness

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, I have removed the finally block.

where TValue : unmanaged, IBinaryInteger<TValue>
{
byte* buffer = number.DigitsPtr;
DecimalIeee754<TValue> unpackDecimal = value.Unpack();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unpack should likely not be an extension method.

DecimalIeee754<TValue> unpackDecimal = value.Unpack();
number.IsNegative = unpackDecimal.Signed;

byte* p = buffer + TDecimal.Precision;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

More cases that should use Span instead. There's a number of them and I don't plan to comment every instance

Comment on lines 33 to 36
private static readonly UInt128 PositiveInfinityValue = new UInt128(upper: 0x7800_0000_0000_0000, lower: 0);
private static readonly UInt128 NegativeInfinityValue = new UInt128(upper: 0xf800_0000_0000_0000, lower: 0);
private static readonly UInt128 ZeroValue = new UInt128(0, 0);
private static readonly UInt128 NegativeZeroValue = new UInt128(0x8000_0000_0000_0000, 0);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this be a problem for codegen since UInt128 is not const?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They should probably be properties instead

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, I have updated it.

return Number.FormatDecimalIeee754<Decimal128, UInt128>(this, format, NumberFormatInfo.GetInstance(provider));
}

private static UInt128[] UInt128Powers10 =>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be a problem for constant embedding. long array can be encoded into executable and read by pairs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-System.Numerics community-contribution Indicates that the PR has been added by a community member new-api-needs-documentation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[API Proposal]: Add Decimal32, Decimal64, and Decimal128 from the IEEE 754-2019 standard.

6 participants