89

I'm confused about why we care about different representations for positive and negative zero.

I vaguely recall reading claims that having a negative zero representation is extremely important in programming that involves complex numbers. I've never had the opportunity to write code involving complex numbers, so I'm a little baffled about why this would be the case.

Wikipedia's article on the concept isn't especially helpful; it only makes vague claims about signed zero making certain mathematical operations simpler in floating point, if I understand correctly. This answer lists a couple of functions that behave differently, and perhaps something could be inferred from the examples if you're familiar with how they might be used. (Although, the particular example of the complex square roots looks flat out wrong, since the two numbers are mathematically equivalent, unless I have a misunderstanding.) But I have been unable to find a clear statement of the kind of trouble you would get into if it wasn't there. The more mathematical resources I've been able to find state that there is no distinguishing between the two from a mathematical perspective, and the Wikipedia article seems to suggest that this is rarely seen outside of computing aside from describing limits.

So why is a negative zero valuable in computing? I'm sure I'm just missing something.

jpmc26
  • 5,489

7 Answers7

106

You need to keep in mind that in FPU arithmetics, 0 doesn't necessarily has to mean exactly zero, but also value too small to be represented using given datatype, e.g.

a = -1 / 1000000000000000000.0

a is too small to be represented correctly by float (32 bit), so it is "rounded" to -0.

Now, let's say our computation continues:

b = 1 / a

Because a is float, it will result in -infinity which is quite far from the correct answer of -1000000000000000000.0

Now let's compute b if there's no -0 (so a is rounded to +0):

b = 1 / +0
b = +infinity

The result is wrong again because of rounding, but now it is "more wrong" - not only numerically, but more importantly because of different sign (result of computation is +infinity, correct result is -1000000000000000000.0).

You could still say that it doesn't really matter as both are wrong. The important thing is that there are a lot of numerical applications where the most important result of the computation is the sign - e.g. when deciding whether to turn left or right at the crossroad using some machine learning algorithm, you can interpret positive value => turn left, negative value => turn right, actual "magnitude" of the value is just "confidence coefficient".

qbd
  • 2,936
13

First, how do you create a -0? There are two ways: (1) do a floating-point operation where the mathematical result is negative, but so close to zero that it gets rounded to zero and not to a non-zero number. That calculation will give a -0. (b) Certain operations involving zeroes: Multiply a positive zero by a negative number, or divide a positive zero by a negative number, or negate a positive zero.

Having a negative zero simplifies multiplication and division a little bit, the sign of x*y or x/y is always the sign of x, exclusive or the sign of y. Without negative zero, there would have to be some extra check to replace -0 with +0.

There are some very rare situations where it useful. You can check whether the result of a multiplication or division is mathematically greater than or less than zero, even if there is an underflow (as long as you know the result is not a mathematical zero). I cannot remember ever having written code where it makes a difference.

Optimising compilers hate -0. For example, you cannot replace x + 0.0 with x, because the result shouldn't be x if x is -0.0. You can't replace x * 0.0 with 0.0, because the result should be -0.0 if x < 0 or x is -0.0.

gnasher729
  • 49,096
7

C# Double which conforms to IEEE 754

    double a = 3.0;
    double b = 0.0;
    double c = -0.0;

    Console.WriteLine(a / b);
    Console.WriteLine(a / c);

prints:

Infinity
-Infinity

actually to explain a little...

Double d = -0.0; 

This means something a lot closer to d= The Limit of x as x approaches 0- or The Limit of x as x approaches 0 from the negatives.


To address Philipp's comment...

Basically negative zero means underflow.

There's very little practical use for negative zero if any...

for example, this code (again C#):

double a = -0.0;
double b = 0.0;

Console.WriteLine(a.Equals(b));
Console.WriteLine(a==b);
Console.WriteLine(Math.Sign(a));

yields this result:

True
True
0

To explain informally, All of the special values an IEEE 754 floating point can have (positive infinity, negative infinity, NAN, -0.0) have no meaning in the practical sense. They can't represent any physical value, or any value that makes sense in "real world" calculation. What they mean is basically this:

  • positive infinity means a an overflow at the positive end a floating point can represent
  • negative infinity means a an overflow at the positive end a floating point can represent
  • negative zero means a an underflow and the operands had opposite signs
  • positive zero may mean a an underflow and the operands had the same sign
  • NAN means your calculation is complacently undefined, like sqrt(-7), or it doesnt have a limit like 0/0 or like PositiveInfinity/PositiveInfinity
AK_
  • 744
6

The question about how this relates to complex-number calculations really gets at the heart of why both +0 and -0 exist in floating-point. If you study Complex Analysis at all, you rapidly discover that continuous functions from Complex to Complex usually cannot be treated as 'single-valued' unless one adopts the 'polite fiction' that the outputs form what is known as a 'Riemann surface'. For example, the complex logarithm assigns each input infinitely many outputs; when you 'connect them up' to form a continuous output, you end up with all of the real-parts forming an 'infinite corkscrew' surface around the origin. A continuous curve that crosses the real axis 'downward from the positive-imaginary side' and another curve that 'wraps around the pole' and crosses the real axis 'upward from the negative-imaginary side' will take different values on the real axis because they pass over different 'sheets' of the Riemann surface.

Now apply that to a numerical program that calculates using complex floating-point. The action taken after a given calculation may be very different depending on which 'sheet' the program is currently 'on', and the sign of the last calculated result probably tells you which 'sheet'. Now suppose that result was zero? Remember, here 'zero' really means 'too small to represent correctly'. But if the calculation could arrange to -preserve the sign- (i.e remember which 'sheet') when the result is zero, then the code can check the sign and perform the right action even in this situation.

PJM
  • 61
2

The reason is simpler than usual

Of course there is a lot of hacks which are looking really nice and they are useful (like rounding to -0.0 or +0.0 but assume we have a representation of signed int with a minus/plus sign at the beginning (i know that is resolved by U2 binary code in integers usually but assume a less complex representation of double):

0 111 = 7
^ sign

What if there is negative number?

1 111 = -7

Okay, that simple. So let's represent 0:

0 000 = 0

That's fine too. But what about 1 000? Is it has to be a forbidden number? Better no.

So let's assume there is two types of zero:

0 000 = +0
1 000 = -0

Well, that will simplify our calculations and fortuately give a some rounding up additional features. So the +0 and -0 are coming from just binary representation issues.

2

One place negative zero is useful is when using hh:mm:ss or Degree:mm:ss to describe a position for instance on the Earth's surface or the Sky. The Hours / Degrees, minutes & seconds are normally held as 3 separate numbers. Degrees to the west of the meridian are normally negative so you may have -5:20:30. The mm & ss are implied to be in the negative (west direction) even though they are signed as positive. The calculation to decimal degrees takes this into account. The problem comes with 00:mm:ss W. If there is no negative zero in the number system used the calculation used for -1 degrees and above will fail. There are work arounds but they require that the problem is recognised and the E or W is saved as a 4th number and used in the calculation, in the case of it being west to negate the result.

hh represents hours, mm minutes and ss seconds as part of an angle

JohnM
  • 29
0

I want to give a somewhat practical example. While this example is constructed to be simple, it reflects applications that I encounter on an everyday basis.

Suppose we want to minimise the function f(x) = exp(1/x) − 1/g(100·x), where g(x) = −(1+x)−x, all for positive x. This function looks like this (and nothing mathematically interesting happens beyond the plotted interval):

plot of f

Below is a simple Python code that implements those functions and applies a numerical minimiser to f. The only thing that can be potentially be negative zero is g(x), so we can simulate a world that only has a regular (positive) zero in a second round:

import numpy as np
from scipy.optimize import minimize_scalar

def is_negative_zero(x): return str(x) == "-0.0"

print(f"−0?\tx\tf(x)") for world_has_negative_zero in [True,False]: def f(x): return np.exp(1/x) - 1/g(100*x)

def g(x):
    result = -(1+x)**(-x)
    if is_negative_zero(result) and not world_has_negative_zero:
        # replaces negative zeros with positive ones
        return np.float64(0.0) # to avoid ZeroDivisionError
    else:
        return result

result = minimize_scalar(f)
print(f&quot;{world_has_negative_zero}\t{result.x:.3f}\t{result.fun:.0f}&quot;)

This returns:

−0?     x       f(x)
True    0.069   3525796
False   5.236   -inf

As you can see, we obtain the correct result only with a negative zero.

Now, what happened here? g(x) is generally negative, but for x > 150, it is larger than the largest negative floating-point number. For such x, a numerical underflow occurs, and g(x) is best represented as negative zero.

This happens in the first round, and consequentially f(x) becomes positive infinity. This is a broad approximation, but it is correct insofar as the value of f(x) is indeed very large, which is all we (and minimize_scalar) need to know for our purposes: Since we did not give minimize_scalar any hints on where to find the minimum, it performs a trial-and-error procedure, during which it calls g with arguments larger than 150. Since the result is infinity, it (correctly) assumes that no minimum can be found there and searches elsewhere, eventually finding the minimum.

In the second round, we make g(x) return positive zero for large x, which leads to f(x) being negative infinity, which is far from the truth. minimize_scalar finds such a case during its initial probing and thus this is the end result, because it trumps the correct minimum.

In many cases involving numerical solvers and similar, numerical over- and underflows are common for bad guesses. Since the floating-point standard correctly handles them (and produces NaNs when it can’t), routines like minimize_scalar can do so as well. Thus the programmers using all of these do not need to handle them separately, and instead this is done without further ado by the innermost layers of the CPU. This does not only considerably speed up certain computations, but also keeps code simple, avoids programming mistakes, and prevents bugs.

Wrzlprmft
  • 198
  • 10