-
Notifications
You must be signed in to change notification settings - Fork 4.9k
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 to Decimal Conversion Refactor (WIP) #70602
Conversation
Tagging subscribers to this area: @dotnet/area-system-numerics Issue DetailsFixes #68042 This is unfinished, but I'm leaving for vacation, so I wanted to share the current state of this effort. Problems with previous codeThe previous conversion code is buggy.
Summary of approachThis PR is a general refactor of the conversion code. The primary goal is to solve the reported bug, but secondarily make this process more efficient by reusing fast code we have written elsewhere. To achieve this, @tannergooding and I brainstormed reusing a section of the Dragon4 algorithm for To do this, I split up the code for the Dragon4 algorithm into two parts and added an additional API that allows Status of the workThe rough draft of the approach is complete, and the original bug reported in #68042 is now fixed. Unfortunately, the logic does not seem to be holding up for all code paths, as there are a number of failing test cases in I haven't had enough time to dive deep into what is causing these issues, but I'm adding GitHub comments to the logic that I think is shaky. What is left to do
|
// | ||
const uint DBLBIAS = 1022; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As mentioned, this is conceptually incorrect, as possibly even logically incorrect
// 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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As mentioned, this is incorrect, and the cause of the original bug
// power is between -14 and 43 | ||
|
||
if (power >= 0) | ||
if (input.Exponent < -94) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm using the Exponent
exposed on double
now. I think this is more correct than the original comparison.
if (X86.Sse41.IsSupported) | ||
mant = (ulong)(long)Math.Round(dbl); | ||
else | ||
if (input.Exponent >= 96) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should prevent all overflowing doubles from making it to Dragon4.
// / 10e, where m is an integer such that | ||
// -296 <; m <; 296, and e is an integer | ||
// / 10^e, where m is an integer such that | ||
// -2^96 <; m <; 2^96, and e is an integer |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
// -2^96 <; m <; 2^96, and e is an integer | |
// -2^96 <= m <= 2^96, and e is an integer |
Is there something I'm missing about the use of ;
here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This comment got corrupted in June 2002. Originally it looked this way:
* <p>The finite set of values of type <i>Decimal</i> are of the form <i>m</i>
* / 10<sup><i>e</i></sup>, where <i>m</i> is an integer such that
* -2<sup>96</sup> < <i>m</i> < 2<sup>96</sup>, and <i>e</i> is an integer
* between 0 and 28 inclusive.
You should use <
, not <=
. Also please move / 10^e
to the previous line.
// require that the DoubleToNumber handle zero itself. | ||
Debug.Assert(mantissa != 0); | ||
|
||
Dragon4State state = Dragon4GetScaleValueMargin(mantissa, exponent, mantissaHighBitIdx, hasUnequalMargins, cutoffNumber, isSignificantDigits); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
First half of Dragon4 is now hoisted to this helper function.
@@ -537,5 +458,171 @@ private static unsafe uint Dragon4(ulong mantissa, int exponent, uint mantissaHi | |||
Debug.Assert(outputLen <= buffer.Length); | |||
return outputLen; | |||
} | |||
|
|||
private static unsafe Dragon4State Dragon4GetScaleValueMargin(ulong mantissa, int exponent, uint mantissaHighBitIdx, bool hasUnequalMargins, int cutoffNumber, bool isSignificantDigits) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here is the hoisted first half of Dragon4
@@ -224,11 +224,48 @@ public void Ctor_Double(double value, int[] bits) | |||
[InlineData(double.MinValue)] | |||
[InlineData(double.PositiveInfinity)] | |||
[InlineData(double.NegativeInfinity)] | |||
[InlineData(79228162514264337593543950335.0)] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This number is decimal.MaxValue, which is actually stored as 79228162514264337593543950336.0 as a double, which has an exponent of 96 and should overflow when cast to a decimal.
[Fact] | ||
public void Ctor_LargeDouble_RoundtripCastSucceeds() | ||
{ | ||
// Decrementing Decimal's MaxValue to get a number that shouldn't lose precision when cast back and forth | ||
double x = Math.BitDecrement(79228162514264337593543950335.0); | ||
|
||
// Cast to a decimal | ||
decimal y = new decimal(x); | ||
|
||
// Use strings to compare values of double and decimal, ensuring no precision loss | ||
string x_string = x.ToString("G99"); | ||
string y_string = y.ToString("G99"); | ||
Assert.Equal(x_string, y_string); | ||
|
||
// Cast back to double, ensuring no precision loss | ||
double z = (double)y; | ||
Assert.Equal(x, z); | ||
|
||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the original bug and is passing now.
[Fact] | ||
public void Ctor_SmallDoubleRoundsUp() | ||
{ | ||
// Create a double with decimal's smallest non-zero value | ||
double x = .0000000000000000000000000001; | ||
|
||
// Cast to a decimal | ||
decimal y = new decimal(x); | ||
|
||
Assert.NotEqual(decimal.Zero, y); | ||
|
||
// Use strings to ensure decimal is correct | ||
string y_string = y.ToString("G99"); | ||
Assert.Equal(".0000000000000000000000000001", y_string); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is meant to test conversion to the smallest non-zero decimal value. WIP.
This reverts commit 1c3556d.
The code is working and complete but needs some cleanup. I will either make a fresh PR or update this one tomorrow. |
Fixes #72135
This is unfinished, but I'm leaving for vacation, so I wanted to share the current state of this effort.
Problems with previous code
The previous conversion code is buggy.
const uint DBLBIAS = 1022;
is conceptually incorrect; the exponent bias fordouble
is1023
double
anddecimal
Summary of approach
This PR is a general refactor of the conversion code. The primary goal is to solve the reported bug, but secondarily make this process more efficient by reusing fast code we have written elsewhere. To achieve this, @tannergooding and I brainstormed reusing a section of the Dragon4 algorithm for
double
tostring
conversion, as printing adouble
already involves converting it to base 10.To do this, I split up the code for the Dragon4 algorithm into two parts and added an additional API that allows
Decimal.DecCalc.cs
access to the first half of Dragon4. This API is structured to return everythingDecimal.DecCalc.cs
needs to construct adecimal
representation of the originaldouble
.Status of the work
The rough draft of the approach is complete, and the original bug reported in #68042 is now fixed. Unfortunately, the logic does not seem to be holding up for all code paths, as there are a number of failing test cases in
DecimalTests.cs
. For example:I haven't had enough time to dive deep into what is causing these issues, but I'm adding GitHub comments to the logic that I think is shaky.
What is left to do