Why computers have two zeros: +0 and -0

Here’s a strange detail of floating point arithmetic: computers have two versions of 0: positive zero and negative zero. Most of the time the distinction between +0 and -0 doesn’t matter, but once in a while signed versions of zero come in handy.

If a positive quantity underflows to zero, it becomes +0. And if a negative quantity underflows to zero, it becomes -0. You could think of +0 (respectively, -0) as the bit pattern for a positive (negative) number too small to represent.

The IEEE floating point standard says 1/+0 should be +infinity and 1/-0 should be -infinity. This makes sense if you interpret +/- 0 as the ghost of a number that underflowed leaving behind only its sign. The reciprocal of a positive (negative) number too small to represent is a positive (negative) number too large to represent.

Windows with Visual C++ returns the same output except Windows prints infinity as 1#INF rather than inf. (See these notes for more on how Windows and Linux handle floating point exceptions.)

There is something, however, about signed zeros and exceptions that doesn’t make sense to me. The aptly named report “What Every Computer Scientist Should Know About Floating Point Arithmetic” has the following to say about signed zeros.

In IEEE arithmetic, it is natural to define log 0 = -∞ and log x to be a NaN when x < 0. Suppose that x represents a small negative number that has underflowed to zero. Thanks to signed zero, x will be negative, so log can return a NaN. However, if there were no signed zero, the log function could not distinguish an underflowed negative number from 0, and would therefore have to return -∞.

This implies log(-0) should be NaN and log(+0) should be -∞. That makes sense, but that’s not what happens in practice. The log function returns -∞ for both +0 and -0.

I dabbled with infinites in C# and Ruby a while back. Now it seems my unnecessary bold statement that Ruby is IEEE 754-1985 compliant could be wrong, because in Ruby (1.8.7) both log(y) above sadly causes a “Numerical result out of range” error.

Thanks for your investigations, Captain. Is it possible that most languages are not IEEE compliant? Did you ever get to grips with William Kahan’s “Why JAVA’s Floating Point Hurts Everyone Everywhere?” enough to know whether the criticisms relate to other languages too?

whether or not you have -0 and +0 is usually dependent on how your hardware represents numbers. I wouldn’t expect -0 or +0 to be a valid numbers on x86 architecture, since I believe it’s two’s compliment.

Joe User, he’s talking about floating point representations of numbers, which is what he means by “IEEE floating point standard” and using “double” as the types of his variables. Modern x86 processors have floating-point units (aka “math coprocessors”) that operate on floating-point numbers — processors without floating-point units must implement all the wacky IEEE754 operations in software rather than in hardware.

I think Goldberg’s essay goes a little astray on the logarithm thing. The same reasoning would lead to a conclusion that sqrt(-0) should be NaN, which I don’t think anyone would advocate. I’m not expert on IEEE arithmetic (which doesn’t specify log(0) in any case, unless that has changed since I last paid attention) but as I understand it, IEEE has a signed zero as a result of a lot of thought about branch cuts for elementary functions of a complex variable, so thinking about it in terms of functions of a real variable doesn’t work very well.

For nonzero z, write z = r*exp(i*t), where r>0 and i is the imaginary unit. The choice of t is indeterminate, but we will have log(z) = log(r) + i*t for one of the possible values of t. As z goes to zero, log(z) goes to minus infinity no matter what the direction of approach. This suggests that log(-0) and log(0) should both evaluate to minus infinity. They should also raise the divide by zero exception, for the reasons given by Kahan in his notes on IEEE 754 (cf. page 10).

The real story with signed zero and the logarithm has to do with discontinuity across the negative real axis with the usual principal value, where you get an extra tiny slice of continuity by a definition like log(-1+0i) = pi and log(-1-0i) = minus pi.

IEEE 754: “Except that squareRoot(–0) shall be –0, every valid squareRoot shall have a positive sign.”

I remember when Fortran 95 added the distinction between +0 and -0 to the Fortran language as an option. I had to change a compiler and libraries. I added a command-line option which allowed +0 and -0 to either be distinguished or not . The only places in the whole compiler/library chain which were affected, were the I/O routines which needed to be able to write +/- 0 and read it back in unchanged, and the SIGN(A,B) intrinsic, which combines B’s sign with A’s mantissa/exponent.

Fortran 77 and 90 mandated that -0 == +0 in all behaviors, so if you wrote -0 out, it would be written the same as +0, and if you read it back in it would be read in as +0, and if you used SIGN(A,-0) it would return ABS(A), just like SIGN(A,+0) would.

If you want to know how special functions should work with non-normal IEEE values, see this draft of the C99 standard, Annex F. It was mostly authored by a former colleague of mine, Jim Thomas. It even covers complex numbers.

Logarithms are (generally, as far as I know) a software operation instead of a hardware one done in the mathematical coprocessor. The result returned by your language can differ from the expected one depending on the software (math library of language used).

I have always considered that those special numbers in the IEEE standard smell sulfur. Willing to continue computations when the floating-point range has been exceeded seems to me both suicidal and of very limited use.

I mean suicidal because I feel it to be a naive attempt to “emulate” the computation of limits. Limits are interesting corner cases where something happens (most of the time this is where your idealized model of nature is flawed), and they deserve careful scrutiny from a competent mathematician. Leaving this to a the silicon neurons of a number-crunching processor is a bit nonsensical.

I mean of very limited use because in the same vein, underterminate forms quickly pop in and you just end-up with no answer. Not a big difference with an earlier out-of-range exception.

In my opinion, just a handy trick for lazy programmers, using the NaNs to detect uninitialized values.

When there are so many ways to represent invalid or ambiguous floating point numbers, why don’t we also get the tools to test for them? I’ve tried to write a test for NaN or +/-inf after calls to mathematical library functions in an attempt to catch errors, but I wasn’t able to come up with a working code.

This article gave me the idea to use sprintf and analyze the result strings. But it would be so much easier if there were simple test functions to test a floating point number for these special cases. It would also help to have constants with these values.

One other reason for the distinction is for functions with branch lines. The best example is the arctangent. Languages usually have two functions for this: atan, which takes one argument, and atan2, which takes two. Java’s Math.atan2 has signature

public double atan2(double y, double x)

Now, given a point P in the complex plane, (x,y), atan2(y, x) gives the signed angle from the positive x-axis to the vector OP. The function has a “branch line” along the negative x-axis; as a point approaches the negative x-atis from above, atan2 tends to π. As it approaches it from below, atan2 tends to −π. Given a function that produces negative zeroes, probably because of branch lines too, one can get the correct arctangent.