Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Double (and Float) to Decimal Conversion Precision Improvement #72217

Closed
wants to merge 34 commits into from
Closed
Show file tree
Hide file tree
Changes from 33 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
391f805
WIP: decimal conversion bug
dakersnar May 3, 2022
a6dc156
Merge branch 'main' of https://github.com/dotnet/runtime into fix-68042
dakersnar May 3, 2022
edc3f67
Merge branch 'main' of https://github.com/dotnet/runtime into fix-68042
dakersnar May 3, 2022
cccae84
Merge branch 'main' of https://github.com/dotnet/runtime into fix-68042
dakersnar May 3, 2022
5a19f23
Merge branch 'fix-68042' of https://github.com/dakersnar/runtime into…
dakersnar May 9, 2022
b0721b8
Split up Dragon4 into two parts in order to reuse the first half later.
dakersnar Jun 3, 2022
d213837
Slight refactor to remove unneccesary calculation from the helper fun…
dakersnar Jun 3, 2022
728efa1
WIP: Decimal.DecCalc
dakersnar Jun 4, 2022
31820e3
Merge branch 'main' of https://github.com/dotnet/runtime into fix-68042
dakersnar Jun 4, 2022
1df3d17
WIP: Number.Dragon4.cs
dakersnar Jun 4, 2022
246483c
WIP: DecimalTests.cs
dakersnar Jun 4, 2022
40fcdd5
Initial unit test now passing
dakersnar Jun 10, 2022
7e6cd93
Updated decimal unit test file
dakersnar Jun 10, 2022
e77161f
WIP: Current state of this fix
dakersnar Jun 11, 2022
29bac4b
Remove unneeded include
dakersnar Jul 5, 2022
94b874b
Possible working solution
dakersnar Jul 6, 2022
6525508
Fix naming, fix typo of tests
dakersnar Jul 6, 2022
895b413
Adjust comments, tweak style, canonicalize result
dakersnar Jul 7, 2022
5d9edfa
Update unit test data to be more accurate for double and float
dakersnar Jul 11, 2022
e6dbd62
Fix edge cases
dakersnar Jul 11, 2022
2d191a1
Fix precision of returned zeros
dakersnar Jul 11, 2022
47ac650
Fix expected values in Decimal's Generic Math tests
dakersnar Jul 11, 2022
6db7625
Fix test data for InsertDecimal and AppendDecimal tests
dakersnar Jul 11, 2022
1d38279
Fix rounding issues
dakersnar Jul 12, 2022
1c3556d
Slight improvement for decimal->double conversion
dakersnar Jul 12, 2022
ceccdb5
Fix rounding
dakersnar Jul 12, 2022
2030e68
Normalize test data
dakersnar Jul 12, 2022
f1d0451
Revert "Slight improvement for decimal->double conversion"
dakersnar Jul 13, 2022
8a28c84
Remove accidental push to DecCalc
dakersnar Jul 13, 2022
6a7266c
Fix Number.BigInteger DivRem Bug
dakersnar Jul 13, 2022
a62767d
Remove round trip tests temporarily until decimal to double conversio…
dakersnar Jul 13, 2022
23b71de
Fix comment typos
dakersnar Jul 14, 2022
ebeee39
Cleanup
dakersnar Jul 14, 2022
7b843fd
Add "ActiveIssue" property to failing test
dakersnar Jul 18, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
344 changes: 33 additions & 311 deletions src/libraries/System.Private.CoreLib/src/System/Decimal.DecCalc.cs
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,6 @@ private ulong Low64
private const int DEC_SCALE_MAX = 28;

private const uint TenToPowerNine = 1000000000;
private const ulong TenToPowerEighteen = 1000000000000000000;

// The maximum power of 10 that a 32 bit integer can store
private const int MaxInt32Scale = 9;
Expand Down Expand Up @@ -1520,340 +1519,63 @@ internal static unsafe void VarDecMul(ref DecCalc d1, ref DecCalc d2)
/// <summary>
/// Convert float to Decimal
/// </summary>
internal static void VarDecFromR4(float input, out DecCalc result)
internal static void VarDecFromR4(float input, out decimal result)
{
result = default;

// The most we can scale by is 10^28, which is just slightly more
// than 2^93. So a float with an exponent of -94 could just
// barely reach 0.5, but smaller exponents will always round to zero.
//
const uint SNGBIAS = 126;
int exp = (int)(GetExponent(input) - SNGBIAS);
if (exp < -94)
return; // result should be zeroed out

if (exp > 96)
Number.ThrowOverflowException(TypeCode.Decimal);

uint flags = 0;
if (input < 0)
{
input = -input;
flags = SignMask;
}

// Round the input to a 7-digit integer. The R4 format has
// only 7 digits of precision, and we want to keep garbage digits
// out of the Decimal were making.
//
// Calculate max power of 10 input value could have by multiplying
// the exponent by log10(2). Using scaled integer multiplcation,
// log10(2) * 2 ^ 16 = .30103 * 65536 = 19728.3.
//
double dbl = input;
int power = 6 - ((exp * 19728) >> 16);
// power is between -22 and 35

if (power >= 0)
{
// We have less than 7 digits, scale input up.
//
if (power > DEC_SCALE_MAX)
power = DEC_SCALE_MAX;

dbl *= s_doublePowers10[power];
}
else
{
if (power != -1 || dbl >= 1E7)
dbl /= s_doublePowers10[-power];
else
power = 0; // didn't scale it
}
VarDecFromR8(input, out result);
}

Debug.Assert(dbl < 1E7);
if (dbl < 1E6 && power < DEC_SCALE_MAX)
{
dbl *= 10;
power++;
Debug.Assert(dbl >= 1E6);
}
/// <summary>
/// Convert double to Decimal
/// </summary>

// Round to integer
internal static void VarDecFromR8(double input, out decimal result)
{
// The smallest non-zero decimal we can represent is 10^-28, which is just slightly more
// than 2^-93. So a float with an exponent of -94 could just
// barely reach 0.5, but smaller exponents will always round to zero.
// This is a shortcut handle this edge case more efficiently.
//
uint mant;
// with SSE4.1 support ROUNDSD can be used
if (X86.Sse41.IsSupported)
mant = (uint)(int)Math.Round(dbl);
else
if (input.Exponent < -94)
{
mant = (uint)(int)dbl;
dbl -= (int)mant; // difference between input & integer
if (dbl > 0.5 || dbl == 0.5 && (mant & 1) != 0)
mant++;
}

if (mant == 0)
return; // result should be zeroed out

if (power < 0)
{
// Add -power factors of 10, -power <= (29 - 7) = 22.
//
power = -power;
if (power < 10)
// If the input is actually zero, we return the smallest precision zero
if (input == double.Zero)
{
result.Low64 = UInt32x32To64(mant, s_powers10[power]);
result = double.IsPositive(input) ? decimal.Zero : new decimal(0, 0, 0, unchecked((int)SignMask));
}
else
{
// Have a big power of 10.
//
if (power > 18)
{
ulong low64 = UInt32x32To64(mant, s_powers10[power - 18]);
UInt64x64To128(low64, TenToPowerEighteen, ref result);
}
else
{
ulong low64 = UInt32x32To64(mant, s_powers10[power - 9]);
ulong hi64 = UInt32x32To64(TenToPowerNine, (uint)(low64 >> 32));
low64 = UInt32x32To64(TenToPowerNine, (uint)low64);
result.Low = (uint)low64;
hi64 += low64 >> 32;
result.Mid = (uint)hi64;
hi64 >>= 32;
result.High = (uint)hi64;
}
}
}
else
{
// Factor out powers of 10 to reduce the scale, if possible.
// The maximum number we could factor out would be 6. This
// comes from the fact we have a 7-digit number, and the
// MSD must be non-zero -- but the lower 6 digits could be
// zero. Note also the scale factor is never negative, so
// we can't scale by any more than the power we used to
// get the integer.
//
int lmax = power;
if (lmax > 6)
lmax = 6;

if ((mant & 0xF) == 0 && lmax >= 4)
{
const uint den = 10000;
uint div = mant / den;
if (mant == div * den)
// Otherwise, we return the maximum precision version of zero or negative zero
uint zeroFlags = 0;
if (double.IsNegative(input))
{
mant = div;
power -= 4;
lmax -= 4;
zeroFlags = SignMask;
}
zeroFlags |= 28 << ScaleShift;
result = new decimal(0, 0, 0, (int)zeroFlags);
}

if ((mant & 3) == 0 && lmax >= 2)
{
const uint den = 100;
uint div = mant / den;
if (mant == div * den)
{
mant = div;
power -= 2;
lmax -= 2;
}
}

if ((mant & 1) == 0 && lmax >= 1)
{
const uint den = 10;
uint div = mant / den;
if (mant == div * den)
{
mant = div;
power--;
}
}

flags |= (uint)power << ScaleShift;
result.Low = mant;
return;
}

result.uflags = flags;
}

/// <summary>
/// Convert double to Decimal
/// </summary>
internal static void VarDecFromR8(double input, out DecCalc result)
{
result = default;

// The most we can scale by is 10^28, which is just slightly more
// than 2^93. So a float with an exponent of -94 could just
// barely reach 0.5, but smaller exponents will always round to zero.
// The smallest double with an exponent of 96 is just over decimal.MaxValue. This
// means that an exponent of 96 and above should overflow.
//
const uint DBLBIAS = 1022;
int exp = (int)(GetExponent(input) - DBLBIAS);
if (exp < -94)
return; // result should be zeroed out

if (exp > 96)
if (input.Exponent >= 96)
{
Number.ThrowOverflowException(TypeCode.Decimal);
}

uint flags = 0;
if (input < 0)
if (double.IsNegative(input))
{
input = -input;
flags = SignMask;
}

// Round the input to a 15-digit integer. The R8 format has
// only 15 digits of precision, and we want to keep garbage digits
// out of the Decimal were making.
//
// Calculate max power of 10 input value could have by multiplying
// the exponent by log10(2). Using scaled integer multiplcation,
// log10(2) * 2 ^ 16 = .30103 * 65536 = 19728.3.
//
double dbl = input;
int power = 14 - ((exp * 19728) >> 16);
// power is between -14 and 43

if (power >= 0)
{
// We have less than 15 digits, scale input up.
//
if (power > DEC_SCALE_MAX)
power = DEC_SCALE_MAX;

dbl *= s_doublePowers10[power];
}
else
{
if (power != -1 || dbl >= 1E15)
dbl /= s_doublePowers10[-power];
else
power = 0; // didn't scale it
}

Debug.Assert(dbl < 1E15);
if (dbl < 1E14 && power < DEC_SCALE_MAX)
{
dbl *= 10;
power++;
Debug.Assert(dbl >= 1E14);
}

// Round to int64
//
ulong mant;
// with SSE4.1 support ROUNDSD can be used
if (X86.Sse41.IsSupported)
mant = (ulong)(long)Math.Round(dbl);
else
{
mant = (ulong)(long)dbl;
dbl -= (long)mant; // difference between input & integer
if (dbl > 0.5 || dbl == 0.5 && (mant & 1) != 0)
mant++;
}

if (mant == 0)
return; // result should be zeroed out

if (power < 0)
{
// Add -power factors of 10, -power <= (29 - 15) = 14.
//
power = -power;
if (power < 10)
{
uint pow10 = s_powers10[power];
ulong low64 = UInt32x32To64((uint)mant, pow10);
ulong hi64 = UInt32x32To64((uint)(mant >> 32), pow10);
result.Low = (uint)low64;
hi64 += low64 >> 32;
result.Mid = (uint)hi64;
hi64 >>= 32;
result.High = (uint)hi64;
}
else
{
// Have a big power of 10.
//
Debug.Assert(power <= 14);
UInt64x64To128(mant, s_ulongPowers10[power - 1], ref result);
}
}
else
{
// Factor out powers of 10 to reduce the scale, if possible.
// The maximum number we could factor out would be 14. This
// comes from the fact we have a 15-digit number, and the
// MSD must be non-zero -- but the lower 14 digits could be
// zero. Note also the scale factor is never negative, so
// we can't scale by any more than the power we used to
// get the integer.
//
int lmax = power;
if (lmax > 14)
lmax = 14;

if ((byte)mant == 0 && lmax >= 8)
{
const uint den = 100000000;
ulong div = mant / den;
if ((uint)mant == (uint)(div * den))
{
mant = div;
power -= 8;
lmax -= 8;
}
}
(uint low, uint mid, uint high, uint scale) = Number.Dragon4DoubleToDecimal(input);

if (((uint)mant & 0xF) == 0 && lmax >= 4)
{
const uint den = 10000;
ulong div = mant / den;
if ((uint)mant == (uint)(div * den))
{
mant = div;
power -= 4;
lmax -= 4;
}
}

if (((uint)mant & 3) == 0 && lmax >= 2)
{
const uint den = 100;
ulong div = mant / den;
if ((uint)mant == (uint)(div * den))
{
mant = div;
power -= 2;
lmax -= 2;
}
}

if (((uint)mant & 1) == 0 && lmax >= 1)
{
const uint den = 10;
ulong div = mant / den;
if ((uint)mant == (uint)(div * den))
{
mant = div;
power--;
}
}

flags |= (uint)power << ScaleShift;
result.Low64 = mant;
}
flags |= scale << ScaleShift;

result.uflags = flags;
// Construct the decimal and canonicalize it, removing extra trailing zeros with a division
result = new decimal((int)low, (int)mid, (int)high, (int)flags) / 1.0000000000000000000000000000m;
}

/// <summary>
Expand Down
Loading