-
-
Notifications
You must be signed in to change notification settings - Fork 742
Fix Issue 14786 - std.math.pow sometimes gets the sign of the result wrong. #3598
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
Conversation
if (w != y) | ||
real absY = fabs(y); | ||
ulong w = cast(ulong)absY; | ||
if (w != absY) |
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.
these three lines look strange. aren't you comparing y with y in the end. isn't the only logical reason for that been != floating point imprecision
@burner The entire current implementation is definitely strange. It's sub-optimal performance-wise, and whoever worked on it last left a comment on the most important part which begins Really I think the whole thing should probably be re-written, but for this pull request I decided to make the minimum change necessary to fix the bug I found. Anyway, after myself staring at the part you highlighted for a while, I eventually remembered why it's there: It's not checking for floating-point imprecision, but rather whether The roots of negative numbers almost always have an imaginary component. So, a complex number would generally be needed to represent the result of |
if (y > -1.0 / real.epsilon && y < 1.0 / real.epsilon) | ||
enum real maxPrecise = ulong.max; | ||
enum real minPrecise = -maxPrecise; | ||
if (y >= minPrecise && y <= maxPrecise) |
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.
Any condition must be removed. We need to implement funcitons
bool floatingPointIsIntegral(F)(const F x) if(isFloatingPoint!F)
bool floatingPointIsOdd (F)(const F x) if(isFloatingPoint!F)
first.
@9il I can see that there are some things in But, do I really have to start that refactoring just to apply this tiny fix? |
@tsbockman Half of |
Looks like current fix with ulong will fail anyway for integer pow greater then |
That looks helpful. I will try re-writing this patch to use
It works as intended for everything up to 80-bit According to the language specification, 80-bit is largest floating point type currently supported by D. Is this incorrect? Is |
Yes.
No. However |
I notice that there are references to The only thing my web search turned up that fits the name, is this weird format: Extended-precision 128-bit. However, the |
7080cf9
to
eea09fb
Compare
|
IBM 128-bit extended precision format is a pair of IEEE double precision numbers whose sum is equal to the extended precision value. The number with greater magnitude is first. This format has the same magnitude range as an IEEE double precision value, but effectively 106 bits of significand precision. |
Or if you prefer an answer in code format, what you have is the following supported natively.
|
What an ugly hack! There is no way that any attempt to support that format in
Per the docs you linked, this is incorrect:
I have updated this PR to document its limitation to 64-bit precision with a Making everyone keep using the current broken implementation of
|
No need to support
Yes, but unittest for the largest and smallest odd pows, largest and smallest even pows should be added, e.g. for |
I already had some unittests, but I realized they were only valid with 80-bit Intel extended I trimmed a couple of redundant instructions from the runtime code, while I was at it. |
97f75ea
to
13978e9
Compare
That's irrelevant to this PR though. The implementation should work regardless of underlying float. :-) |
13978e9
to
9cf8042
Compare
Use wolfram to get the results for the tests if you haven't already done so. I cannot stress how important is to use a reliable source for unittests vs. whatever number returns when you run it on your machine. |
The issue is that this code: enum ulong_dig = 8 * ulong.sizeof;
static assert(real.mant_dig <= ulong_dig);
enum real maxOdd = ulong.max >> (ulong_dig - real.mant_dig);
const real absY = fabs(y);
if (absY <= maxOdd)
{
const ulong w = cast(ulong)absY;
if (w != absY)
return sqrt(x); // Complex result -- create a NaN
if (w & 1)
sign = -1.0;
}
x = -x; Is considerably faster than its format-agnostic equivalent, because floating-point modulus is slow: // Untested; this is just an example
const real ym2 = fabs(y) % 2.0L;
if(ym2 == 1.0L)
sign = -1.0;
else if(ym2 != 0.0L)
return sqrt(x); // Complex result -- create a NaN
x = -x; I could use a I can do this if you really want, but I don't think intentionally shipping dead code is a good idea. |
This is not that kind of math - my unittest basically just says, "Negative one to an odd power should return negative one." My error was in hard-coding the maximum odd value, without taking into account the possibility of other |
I'm fine with having a fast path for 80-bit reals. I did some further testing (also because I'm a skeptic of Of course, this should be considered slower than
|
The
I was able to optimize your method further: if(x < 0)
{
if(floor(y) != y)
return sqrt(x); // Complex result -- create a NaN
// For negative x, find out if the integer exponent is odd or even.
const hy = scalbn(y, -1);
if(floor(hy) != hy)
sign = -1.0;
x = fabs(x);
} However, in my micro-benchmark even this streamlined version is still over 4x slower than the EDIT: In case anyone wants to know, I tested primarily with GDC. The gap is smaller with DMD, but the conclusion is the same. The LDC results are more ecclectic... |
Since there seems to be strong desire for a generic solution, I should probably mention that I have a proof-of-concept which is by far the fastest in my testing, beating even the bool floatingPointIsRound(F)(const F x, const int expOf2 = 0)
if(isFloatingPoint!F)
{
alias FP = FloatParts!F;
FP xp; xp.num = x;
const xe = xp.exp;
if(xe == FP.traits.expNaN)
return false;
const fde2 = FP.traits.fracDig + expOf2;
if(xe >= fde2)
return true;
if(xe < expOf2)
return xp.mant == 0;
const remMask = ~(~cast(xp.Mant)0 << (fde2 - xe));
static if(FP.traits.isWholeExplicit)
return (xp.mant & remMask) == 0;
else
return (xp.frac & remMask) == 0;
} This code should work correctly and very quickly for any vaguely normal floating-point format (i.e., not However, in order to express the algorithm so directly and cleanly, I had to re-write module floatparts;
import std.traits : Unqual, isFloatingPoint;
import std.math : RealFormat;
template floatTraits(F)
if(isFloatingPoint!F)
{
alias Format = RealFormat;
enum mantDig = F.mant_dig;
enum fracDig = mantDig - 1;
static if(mantDig == 11) {
enum format = Format.ieeeHalf;
enum usedBytes = 2;
alias MantInt = ushort;
enum mantOffset = 0;
enum isWholeExplicit = false;
enum expDig = 5;
enum expOffset = fracDig;
} else static if(mantDig == 24) {
enum format = Format.ieeeSingle;
enum usedBytes = 4;
alias MantInt = uint;
enum mantOffset = 0;
enum isWholeExplicit = false;
enum expDig = 8;
enum expOffset = fracDig;
} else static if(mantDig == 53 && F.sizeof == 8) {
enum format = Format.ieeeDouble;
enum usedBytes = 8;
alias MantInt = ulong;
enum mantOffset = 0;
enum isWholeExplicit = false;
enum expDig = 11;
enum expOffset = fracDig;
} else static if(mantDig == 53 && F.sizeof == 12) {
enum format = Format.ieeeExtended53;
enum usedBytes = 10;
alias MantInt = ulong;
enum mantOffset = 64 - mantDig;
enum isWholeExplicit = true;
enum expDig = 15;
enum expOffset = 64;
} else static if(mantDig == 64) {
enum format = Format.ieeeExtended;
enum usedBytes = 10;
alias MantInt = ulong;
enum mantOffset = 0;
enum isWholeExplicit = true;
enum expDig = 15;
enum expOffset = mantDig;
} else static if(mantDig == 113) {
enum format = Format.ieeeQuadruple;
enum usedBytes = 16;
alias MantInt = ucent;
enum mantOffset = 0;
enum isWholeExplicit = false;
enum expDig = 15;
enum expOffset = fracDig;
} else
static assert(false, "Unknown floating-point format for " ~ F.stringof);
enum mantMax = ~cast(MantInt)0 >> (MantInt.sizeof*8 - mantDig);
enum fracMax = ~cast(MantInt)0 >> (MantInt.sizeof*8 - fracDig);
enum expBias = ~0 >>> ((int.sizeof*8) - (expDig - 1));
enum expMin = -expBias;
enum expMax = expBias;
enum expNaN = expMax + 1;
}
union FloatParts(F)
if(isFloatingPoint!F)
{
public:
alias traits = floatTraits!F;
alias Mant = traits.MantInt;
F num = F.nan;
alias num this;
/+this(F num) {
this.num = num; }+/
@property Mant frac() const {
return (mantBits & fracMask) >> traits.mantOffset; }
@property void frac(Mant newFrac) {
mantBits = (mantBits & ~fracMask) | ((newFrac << traits.mantOffset) & fracMask); }
static if(traits.isWholeExplicit) {
@property Mant whole() const {
return (mantBits & wholeMask) >> wholeOffset; }
@property void whole(Mant newWhole) {
mantBits = (mantBits & ~wholeMask) | ((newWhole << wholeOffset) & wholeMask); }
@property Mant mant() const {
return (mantBits & mantMask) >> traits.mantOffset; }
@property void mant(Mant newMant) {
mantBits = (mantBits & ~mantMask) | ((newMant << traits.mantOffset) & mantMask); }
alias explicit = mant;
} else {
@property uint whole() const {
return (lastBits & expMask) != 0; }
@property Mant mant() const {
return (cast(Mant)whole << wholeOffset) | frac; }
alias explicit = frac;
}
@property int exp() const {
return ((lastBits & expMask) >> expShift) - traits.expBias; }
@property void exp(int newExp) {
lastBits = (lastBits & ~expMask) | (((newExp + traits.expBias) << expShift) & expMask); }
@property int sign() const {
return cast(short)(lastBits & 0x8000) >> 15; }
@property void sign(int newSign) {
lastBits = (lastBits & 0x7FFF) | (newSign & 0x8000); }
@property bool isFinite() const {
return (lastBits & expMask) != expMask; }
@property bool isNormal() const {
switch(lastBits & expMask) {
case 0:
return mantBits == 0;
case expMask:
return false;
default:
return !(traits.isWholeExplicit && (mantBits & wholeMask) == 0);
}
}
@property bool isZero() const {
static if(traits.usedBytes >= (Mant.sizeof + ushort.sizeof))
return (mantBits == 0) && ((lastBits & ~0x8000) == 0);
else {
ptrdiff_t u16x = allBits.length - 1;
if(lastBits == 0x8000)
--u16x;
while(u16x > 0) {
if(allBits[u16x] != 0)
return false;
--u16x;
}
return true;
}
}
ref typeof(this) makeZero() {
ushort keepSign = lastBits & 0x8000;
mantBits = 0;
lastBits = keepSign;
return this;
}
ref typeof(this) makeZero(int newSign) {
mantBits = 0;
lastBits = (newSign & 0x8000);
return this;
}
@property bool isNaN() const {
return (lastBits & expMask) == expMask && frac != 0; }
@property bool isSignaling() const {
return (mantBits & quietMask) == 0; }
ref typeof(this) makeNaN(bool quiet = true) {
lastBits = lastBits | expMask;
enum nanMant = wholeMask | (cast(Mant)1 << traits.mantOffset);
if(quiet)
mantBits = mantBits | quietMask | nanMant;
else
mantBits = (mantBits & ~quietMask) | nanMant;
return this;
}
@property bool isInfinite() const {
return (lastBits & expMask) == expMask && frac == 0; }
ref typeof(this) makeInfinite() {
lastBits = lastBits | expMask;
mantBits = (mantBits & ~fracMask) | wholeMask;
return this;
}
private:
enum Mant fracMask = traits.fracMax << traits.mantOffset;
enum wholeOffset = traits.fracDig + traits.mantOffset;
enum Mant wholeMask = traits.isWholeExplicit? cast(Mant)1 << wholeOffset : 0;
enum Mant mantMask = wholeMask | fracMask;
enum expShift = 15 - traits.expDig;
enum expMask = 0x8000 - (1 << expShift);
enum quietMask = cast(Mant)1 << (wholeOffset - 1);
Mant mantBits;
static assert(traits.usedBytes % 2 == 0);
ushort[traits.usedBytes / 2] allBits;
@property ushort lastBits() const {
return allBits[$ - 1]; }
@property void lastBits(ushort newBits) {
allBits[$ - 1] = newBits; }
} If there is interest, I could potentially finish this and refactor |
There are a few things I can see wrong with that. Feel free to raise a PR. Bearing in mind that this PR exists too: #3499 |
@ibuclaw As I said, I am willing to add a slow, generic path (either Is that what you want? |
@ibuclaw @9il Normally it does not even get compiled, of course, but I have tried to test it myself by temporarily commenting out the faster Good enough? |
Looks good |
long w = cast(long)y; | ||
if (w != y) | ||
enum maxOdd = pow(2.0L, real.mant_dig) - 1.0L; | ||
static if(maxOdd > ulong.max) { |
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.
Wouldn't this static if condition always be false? The only way it could ever succeed is if CTFE started supporting ucent
.
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.
ulong.max
converted to real
I added that fall-back algorithm because you said,
|
Yep, I saw that after I posted. :-) |
@ibuclaw Would you say this is ready to merge? |
I'm fine with this (sorry I've been busy with 2.067) (N.B. It looks like |
@9il You've already approved the latest version, yes? Anyone want to toggle auto-merge on? |
Typical. :-p |
const hy = ldexp(y, -1); | ||
if(floor(hy) != hy) | ||
sign = -1.0; | ||
} else { |
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.
Actually, sorry to be a bother, but curly braces on the next line. This format is used everywhere.
static if(...)
{
}
else
{
}
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.
... and rebase please.
I'll re-test on my arm box just to be sure and will merge depending on when my last comment is addressed. |
a26ce71
to
4b690ef
Compare
It should be fixed now. (I amended the last commit.) |
@ibuclaw The corrected version has now passed all my tests, and the auto-tester. |
Auto-merge toggled on |
Fix Issue 14786 - std.math.pow sometimes gets the sign of the result wrong.
Thanks. |
… the result wrong. Backport of dlang#3598
This is a simple fix for Phobos Issue #14786.
Negative one raised to any odd power should yield negative one.
However, when using 80-bit real arithmetic, -1 raised to a power greater than 2^^63 and less than 2^^64 yields +1.
This is despite the fact that integers in this range can still (just barely) be represented without loss of precision by an 80-bit real; even and odd can still be distinguished.
Thanks to @ibuclaw for pointing me to the relevant code.