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.

To demonstrate this, run the following C code.

int main()
{
    double x = 1e-200;
    double y = 1e-200 * x;
    printf("Reciprocal of +0: %gn", 1/y);
    y = -1e-200*x;
    printf("Reciprocal of -0: %gn", 1/y);
}

On Linux with gcc the output is

Reciprocal of +0: inf
Reciprocal of -0: -inf

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 ran the following code on Linux and Windows.

int main()
{
    double x = 1e-200;
    double y = 1e-200 * x;
    printf("Log of +0: %gn", log(y));
    y = -1e-200*x;
    printf("Log of -0: %gn", log(y));
}

On Linux, the code prints

Log of +0: -inf
Log of -0: -inf

The results were the same on Windows, except for the way Windows displays infinities.

Related link:

IEEE floating point exceptions in C++
Anatomy of a floating point number
Math library functions that seem unnecessary

19 thoughts on “Why computers have two zeros: +0 and -0

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

    C# outputs the same as C++

    double x = 1e-200;
    double y = 1e-200 * x;
    System.Console.WriteLine("Log of +0: {0}n", Math.Log(y));
    y = -1e-200 * x;
    System.Console.WriteLine("Log of -0: {0}n", Math.Log(y));

    Log of +0: -INF
    Log of -0: -INF

  2. Ironically both -0. and +0. are equal to 0. and equal among them selves.
    ie. y==0. is true in both cases!
    if particular -0. in no less than 0.
    y<0. is false

    regarding log, maybe it makes sense define log(-0) as -inf, from the point of view of the algorithm of log. which probably ignores the sign bit after checking that the input is no less than 0.

    maybe interpreting the IEEE -0. as a limit is wrong, to me it is more like a partial nan.

  3. 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?

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

    http://en.wikipedia.org/wiki/IEEE_754-1985
    http://en.wikipedia.org/wiki/IEEE_754-2008

    You are correct about x86’s using two’s complement for integers, though.

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

  6. Actually sqrt(-0) = -0.

    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.

    Fortran 95 changed this, allowing signed 0 distinctions. However, -0 == +0 still (-0 and +0 compare equal).

    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.

  7. -0 and +0 exist in mathematics too.

    Consider the Laplace Transform when applied to, for example, the Dirac delta function. In this case the lower bound is -0, and it’s well defined in terms of a limit.

  8. 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).

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

    I’d be pleased that someone explains me what I misunderstood.

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

  11. I’m having some hard time understanding what’s the point of -0 and +0 – what is the usage for these two values?

    Can you give us an example on when should you care about have – or + in front of a zero?

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

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>