Hacker News new | ask | show | jobs
by bendiksolheim 1936 days ago
Does anyone have a good example of a programming problem where we _need_ signed zero? Or where it makes things significantly simpler? As far as I know, there is no distinction between -0 and +0 in math, so I have never really understood why this is a thing in computers.
14 comments

When does this need arise? Well, otherwise inverting a value can change it's sign and in particular inverting -∞ twice will give you +∞, and being off by "2∞" is a pretty large error for a lot of computations ;)

You can end up with zeros and infinities pretty easily because you overflow or underflow the range of floating point precision, and generally you want something sensible to happen in typical cases, even if some common arithmetic identities necessarily break down.

I would actually like a true signed zero (or rather "epsilon" value ), so -0 and +0 as distinct from "normal" 0 which is truly unsigned, neither positive nor negative. The former two would only arise from underflow, and the reason this is useful that if you underflow from below zero and invert that you want to get -∞ and if you underflow from above zero and invert that you want to get +∞. Inverting a signless zero should give NaN (instead it gives +∞, which is nonsense in basically any case where the domain is not inherently the non-negative reals already and the 0 did not come about by and underflow; in particular 1/0 should be NaN).

If anyone knows why this design was not chosen and what fundamental downsides it has, I'd love to hear it. Obviously representing three zeros is a tad more annoying, but IEEE754 has a lot of stuff that's annoying implementation wise but was added for nicer numerical behavior (e.g. denormals, and of course various "global" rounding modes etc. which probably qualify as a mistake in retrospect).

> If anyone knows why this design was not chosen and what fundamental downsides it has, I'd love to hear it.

No fundamental ones, but a few practical.

32-bit numbers have 2^32 unique values, the number is even. Your approach makes the range asymmetrical like it happens with integers.

The range for 8-bit signed integers is [ -128 .. +127 ]. On ARM NEON there’re two versions of integer negate and absolute instructions, some (like vqnegq_s8 or vqabsq_s8) do saturation i.e. transform -128 into +127, others (vnegq_s8, vabsq_s8) don’t change -128. Neither of them is particularly good: the saturated version violates -(-x) == x, non-saturated version violates abs(x)>=0. Same applies to the rest of the signed integers (16, 32, 64 bits), an all modern platforms.

With IEEE floats the range is symmetrical and none of that is needed. Moreover, PCs don’t have absolute or negate instructions, but they instead have bitwise instructions processing floats, like andps, orps, xorps, andnps, they allow to flip, clear or set just the sign bit, very fast.

Another useful property of IEEE representation is that for two intervals [ 0 .. FLT_MAX ] and [-FLT_MAX .. -0.0f ] sort order of floats corresponds to [inverted] sort order of 32-bit integers.

> Your approach makes the range asymmetrical like it happens with integers.

Not necessarily since you've got (a lot of different) NaNs anyway. For the sake of argument, you could give up one one of them and make it positive zero (since this representation would be unnatural, it would slow stuff down, just as non-finite values do on many CPUs. Wouldn't matter that much since signed zeros would only arise from underflow).

> Another useful property of IEEE representation is that for two intervals [ 0 .. FLT_MAX ] and [-FLT_MAX .. -0.0f ] sort order of floats corresponds to [inverted] sort order of 32-bit integers.

I'm aware, but as you correctly note this only works in the right direction for unsigned values. And it's just not that important a benefit, I'd much rather have my calculations come out right than being able to sort positive floating point numbers with an integer sort routine.

> you could give up one one of them and make it positive zero

What do you expect to happen when you set the sign bit of that number? A possible answer to that is "negative zero", and now you have 2 separate encodings for negative zeroes: one of them with exponent 0, another one 0xFF, and they behave slightly differently.

If you manually screwed around with the most significant bit of the underlying bit pattern (rather than just writing -x like any normal person) I'd expect to get a NaN -- assuming of we keep an explicit sign bit in the representation at all. It would obviously make hardware implementation of negation more complex (and thus slower) but I don't think there is any conceptual problem.
PC hardware doesn’t have hardware implementation of negation.

Possible to do in 2 instruction, xorps to make zero, then subps to subtract. Combined, they gonna take 4-5 cycles of latency (xorps is 1 cycle, subps is 3 cycles on AMD, 4 cycles on Intel).

If you do that a lot, a single xorps with a magic number -0.0f gonna negate these floats 4-5 times faster. People don’t pay me because I’m a normal person, they do that because I write fast code for them :-)

On a serious note, I’d rather have the current +0.0 and -0.0 IEEE values to be equal and be the exact zero, and make another one with 0xFF exponent encoding inexact zeroes, +0.0f or -0.0f depending on the sign bit.

Or another option, redefine FLT_MIN to be 2.8E-45, and reuse the current FLT_MIN, which is 1.4E-45 / 0x00000001 bit pattern, as inexact zeroes.

I wonder if it would be useful to have a signed integer that has symmetric range and the one that is left is used as NaN. Overflows would set it to NaN for example. Then again, once that's on the table it's very tempting to steal two more values for +/- inf. I think it's very useful to have full range unsigned ints but signed ones could have range reduced to make them less error prone.
Posits handle this by using the smallest non-zero number where floating point would go to zero. They also use the largest represent able number instead of infinity. These exist for both positive and negative numbers. At least that's the way I read it.
Co-inventor of posits here, this is basically correct, there still is "infinity" in posits, it's strictly reachable by inverting 0 directly.
Having a single (unsigned) infinity (and a single zero!) seems cleaner in some ways (and I dimly seem to recall that some pre-ieee754 floating point hardware worked that way). On the other hand, having e.g. a neutral element for both max and min also seems pretty nice to have, although without infinities, the maximal and minimal floating point value will equally do the trick in most cases.

How do inequalities work for posit infinity? Is posit infinity both larger and smaller than any other posit?

I thought that was essentially NaN and could also result from square root of negative numbers?
a cons of that is that there is no longer a distinction between positive and negative infinity
So no then? As you say, epsilon should be used in this case.
I meant as in infinitesimal, not as in machine epsilon (which lacks the required properties) -- poor wording on my part. If you have numbers of impossibly large magnitude you probably also want corresponding numbers of impossibly small magnitude. You can do this in a nice, algebraically satisfying way with infinitely many such numbers (hyperreals), but I think if it weren't for the signless zero and positive zero conflation, IEEE754's way would be quite a reasonable finite precision approximation to this.
∞ is not a number. Using it as a number is a hack invented by mathematicians.
∞ is not used as a number by mathematicians. Maybe by engineers.
That is not entirely correct. Schmieden and Laugwitz for example developed in the 1950s a nonstandard Analysis which adjoins an infinitely large element (called Ω) to the natural numbers. The basic idea was a formula A(Ω) was true if A(n) was true for almost all finite natural n.

While it wasn't immensely useful going forward, it helped to clarify the use of infinity and infinitesimals in earlier work.

I'm well aware of nonstandard analysis, but ∞ is still not a number there, even though there are infinitely many infinitely large elements .
The extended complex plane is a space where inf is actually number and where division by zero is allowed.

Same with the extended real line.

Infinity is as much a number as it is useful to define it as such.

Ehh.

https://en.wikipedia.org/wiki/Extended_real_number_line

NB this is hardly nonstandard analysis.

That's right. It's a symbol. When you see it in an expression, you're probably expected to interpret it in light of a limit of some kind. It's just convenient to write it into an expression rather than use cumbersome limit notation all over the place.

Like many notational shortcuts, it's a hack supported by proof. ;-)

This explanation feels off, to me. Aren't all numbers symbols? Pi, e, 10, 0xA, 9.99999...?
In my view, numbers are numbers, and symbols are symbols. There's an agreement that a symbol represents a number, but there's not a one-to-one relationship between available symbols and available numbers. Normally this isn't a problem, and we treat them interchangeably. And indeed the distinction may only be a philosophical oddity or a matter for mathematicians. But I believe nonetheless that there is a distinction.

Now I was merely an undergrad math major, which means I topped out before learning this stuff in a formal way. But at my primitive level of understanding, I think of a number as something that behaves like a number within a given system. What I learned in my courses was how different kinds of numbers behaved: Whole numbers, reals, complex, vectors and tensors, etc. I remember reading a definition of "tensor" that was to the effect of: A tensor is something that behaves like a tensor, meaning that the important thing is the behavior.

Another post in this page expressed that we should be particularly cautious when dealing with numbers, symbols, and IEEE floats, notably to beware that IEEE floats and real numbers don't always behave the same. That was treated in one of my math courses, "Numerical Analysis." You could also get CS credit for that course, suggesting its practical importance.

I think the consequences of what you are saying makes sense. Would be neat to explore more of the idea. I was starting to find, recently, that it was better to think of numbers as symbols that follow operational rules. This view seems counter to that.
Yes, in a way. What distinguishes irrational numbers from rational numbers is that all rational numbers can be represented by strings drawn from a regular language. For example, all strings generated by the regular language "-?\d+\.\d+?_\d+" (where "_" denotes the repeating decimal expansion as in 1/6 = 0.1_6) correspond to exactly one rational number and all rational numbers correspond to at least one string in this regular language. Irrational numbers (and other types of "numbers" such as +inf and -inf) cannot be represented by any such regular language.
I'm not entirely sure I follow. Aren't pi and e irrational numbers?

I also included .9_ as it is an easy trap to show we have two ways of writing 1 in standard decimal notation.

Please read this whole post as a question. I'm genuinely not clear on the distinction.

But not all symbols are numbers.
Why not? One of the profound effects of computers is that we have moved computing to be able to work with images. Sounds. Etc.

I get that not all have simple algebraic concepts.

I get the impression I'm corrupting parts of I am a Strange Loop? Would love to read more on this.

I was taught that you’re supposed to read $y = \inf$ as:

    y = lim_{x->\inf} x
Basically, $y$ isn’t necessarily infinity, but just a number larger than you could ever write. The number at the end of the number line if it existed.
Latitude / Longitude coordinate as floating point.

https://gis.stackexchange.com/questions/211796/if-degrees-is...

I made the above post due to a bug in the GPS on our routers. We had a customer close to the meridian in England.

http://www.thegreenwichmeridian.org/tgm/articles.php?article...

Western hemisphere is negative longitude, eastern hemisphere is positive. If your degrees are 0 (within ~100km? I can't remember exactly), then you still need to know if you're east or west of the meridian.

Computers (obviously) have to approximate the majority of actual mathematical numbers, as they do not have infinite storage.

If you've got two numbers, +0.0000001 and -0.0000001, but you can't represent that precision, can you see how it's less bad to round to +0.0000 and -0.0000 rather than to just 0.0000? It's encoding strictly more information.

Reading your comment gave me a realization that transformed my understanding of floats.

What just clicked for me was that in any system where we use finite precision to store exponents, we can't actually have zero as a normal value... we'll always underflow the exponent before we get to actual mathematical zero.

So +0/-0 is actually a convenient misnomer. They really are +/- epsilon.

The only way to have zero is if it's some specially handled value like NaN. Which IEEE doesn't do and that's entirely understandable.

Makes sense why you can never compare a subtraction of floats to zero (it's beyond just "rounding errors") and the existence of +0/-0 seems quite natural now.

> The only way to have zero is if it's some specially handled value like NaN. Which IEEE doesn't do and that's entirely understandable.

Wait what? Am I missing something? 0 is absolutely part of the IEEE 754 spec thanks to the existence of denormalized floating point numbers. So I would certainly call it a "specially handled value", in a sense. The existence of +/- 0 has more to do with the implementation of the leading sign bit.

The same is true on the other end of the limit. Infinity in floating point really just means "some value larger than can be represented".
> it`s less bad

Really good point. My approach to this was "if it’s not used in any mathematical algorithms, why do we need it in computers?". But in your example, you retain some information even though you can’t represent the whole truth. Thanks!

Ok, so what's true? -0<+0 or -0==+0
You can look these things up for yourself - they're standardised in most language's implementations in something called IEEE 754. In the cases you've asked about they're false and true. Is this what you want in all cases? No. Is this what you want in some cases? Yes. It's a tradeoff. You can still observe the difference by dividing by zero (which should be another indication that these aren't real numbers as we're conventionally understand them.)
> they're standardised in most language's implementations in something called IEEE 754

There is the IEEE 754, and there is the language's standard. One should always look at the latter because it's often the case that the language doesn't fully conform to IEEE 754.

You shouldn't really use equality on floating point numbers, except on very special circumstances (and I imagine the == behavior for 0 breaks things more often than it helps). But the wikipedia page on -0 has your case covered:

> According to the IEEE 754 standard, negative zero and positive zero should compare as equal with the usual (numerical) comparison operators, like the == operators of C and Java. In those languages, special programming tricks may be needed to distinguish the two values

> You shouldn't really use equality on floating point numbers, except on very special circumstances.

This is very common advice, so common that it gets cargo-culted into situations where it is really quite poor.

Information storage, retrieval, and transmission systems should faithfully deliver floating-point values that are good to the last bit. Round-trips through databases, transmission over network protocols, etc should all give values back that are exactly identical to what was put into them.

Oh, sure. But those applications should also not use the floating point equality operators. They deal with opaque data, and should make sure not to corrupt it.

Keep in mind that the ISO standard does require that floating point equality tests return false for values that have the exact same binary representation, and that not everything adheres to it and some environments may give you false for identical values even when the standard says it should be true. Also, != is not the negation of == for floating point. So even using those operators to test a round trip over those applications is iffy.

By "the last bit" do you mean the 32nd bit or the 64th bit? :-)

Many times I've tracked down the place in our stack where a double-precision value from user input accidentally goes through a single-precision variable in some C code somewhere and crashes some Python code later on because the values don't match "to the last bit" in the way that the programmer thought... But that's a bug in the C code - I agree completely the the system SHOULD give the value back that was put into it!

This is actually exactly what I mean. Its probably the most common bug I've come across in this class. I don't expect unit tests to capture all rounding bugs (say, due to serialization and de-serialization through text). But I do expect to capture gross errors, such as an inadvertent cast to lower-precision somewhere in the pipeline.

I've worked with highly experienced and accomplished software engineers that expected interchange through protobuf or sql to be inaccurate due to rounding. No! If you stick a finite number in, you should get the exact same finite number back out again. Direct equality is fine for most cases. The sign bit of zero and NaN should also be returned faithfully and tested using memcmp when required.

IMO, the payload bits of NaN should also be also returned faithfully, but too many systems in common practice drop them.

> Round-trips through databases, transmission over network protocols, etc should all give values back that are exactly identical to what was put into them.

Yes that's true... but what's that got to do with using an equality operator?

IEEE 754 also defines a total order of all fp values, where -0.0 immediately precedes 0.0
They're equal. But, copysign(1.0, -0.0) == -1.0
One should usually not rely on this, unless your language gives guarantees. I don't think even IEEE 754 gives you the guarantee you are implying.

To give you an idea, the C99 standard does not require signed zeros, and if you have them, does not dictate the behavior you are describing. I once worked on a commercial C compiler and we produced a new version that resulted in a change of sign of zero for the exact same computation (and even "worse", would give a different sign on different machines for the same compiler version). We discussed this thoroughly and decided it was OK because our docs made it clear we conform to C99 (which provides no guarantees on signed zero).

More information isn't better if that information isn't useful. Does x*0 evaluate to 0.0? Not with -0 around. Does x+0 evaluate to x? Maybe!
IEEE 754 requires that -0.0 and 0.0 compare equal.

> Does x*0 evaluate to 0.0

No, but it will compare equal, unless x is either infinite or a NaN.

> Does x+0 evaluate to x? Maybe!

Yes, unless x is either infinite or a NaN.

No, if x = -0.0 then x+0 = 0.0 and x*0 = -0.0. This violates fundamental tenets of arithmetic, surprises most, and makes many compiler optimizations impossible.
You could still use the bitwise -0 value to represent another real number. Do you gain anything?
Negative zero was introduced because of branch cuts: https://people.freebsd.org/~das/kahan86branch.pdf
I agree with many of the other replies, but I would also add, perfectly serious and with no sarcasm, do not be fooled; computers do not use "math" numbers. They use what they use; machine-word bounded integers and IEEE floats most commonly, unbounded integers, some variants on rational numbers, and sometimes some more exotic things, but they never use real numbers. They're bounded to the computable numbers.

So whether or not there's a -0 or +0 in some "math" isn't relevant, because computers aren't using "some math" but very specific mathematical constructs that must be understood on their own terms. I haven't seen very many mathematical systems that have a reified "NaN" object that can be explicitly passed around as a valid value to functions. (I know of many systems that have a "bottom" but I would say bottom is usually presented not as an object you can "have" but as a property of some mathematical object. Haskell for instance uses this idea; there are some trivial ways to have or produce something that has the property of being "bottom", like calling "error", but you can't just "have the bottom value".)

Moreover, there are mathematical systems in which such things can appear. There are many exotic and interesting number systems in the mathematical world, which even a bachelor's degree in mathematics may only scratch the surface of, depending on which junior/senior level courses you take. Real numbers are without a doubt the most studied, and they do not contain a -0 (though even then you'll still see it show up sometimes as a special notation in limits), but they are the beginning of number systems, not the end.

I mention all this because it's important; it's a very common misconception that computers use the numbers like you learned in school, and it will lead to nothing but pain. The next misconception is that, ok, sure, they aren't those numbers exactly, but they're so close that I don't have to worry about it. This works as long as you don't push your floats too hard, and a lot of us don't, but also breaks down surprisingly quickly. It's important for programming professionals to understand in general that IEEE floats are their own thing. Whenever I use them I do at least take a couple of seconds to double-check mentally that the sharp pointy bits aren't going to stab me, even for simple things like adding durations of a request to some floating point accumulator for metric purposes.

Floating point math was devised to be used in numerical methods that are applied iteratively, like gradient descent. In that world what you are always doing is "converging" to solutions step by step, and a negative or positive zero can tell you which direction you're converging from.
It’s used when writing SIMD code all the time to quickly mask bits as needed or to check the sign of a floating point number with an SSE intrinsic. _mm_xor_ps(_mm_set1_ps(-0.f), reg) as an example negates all four components of reg.
Sign/magnitude ADCs almost have signed zero in hardware, by generating a sign bit and zero or more magnitude bits. The typical method to map to voltages doubles the span between adjacent values and skips over zero. So a 2-bit sign/magnitude ADC measures values in the series {-3, -1, 1, 3}.

So strictly speaking this number system doesn't have a representation of zero at all. Tiny measurements either round up or round down to +/- 1.

Amusingly enough the Apple Watch reports temperature as 0° and sometimes as -0°, I've not determined what causes that, perhaps rounding around 0?
Sometimes the sign is used to indicate which "direction" the temperature is moving. If it was -10° overnight and it's -0° now, the puddles outside will still be frozen. If it was 10° overnight and it's 0° now, the puddles will still be liquid.

(Edit: no idea whether this applies to the Apple watch, it's just a use case for -0 with regards to temperature.)

Your predictions about the state of the puddle are most likely right, but not for the reasons that you think.

Air temperature is commonly measured at 2m above ground. An measurement of 0° air temperature does not imply 0° ground temperature. The more significant effect is that the ground has a higher thermal capacity than the air, so it changes temperature more slowly throughout the day. If it's been colder before and now it's 0°, the ground is still way below 0° and thus puddles are frozen. If it was warmer and now the air has cooled down to 0°, the ground is going to be a bit warmer still and thus puddles are liquid.

Denoting this difference as +0° and -0° does not seem very useful since the same effect is going to be nearly equally significant at 1° or -2°.

(Sidenote: The thermal capacity of the ground is also the reason why air temperature is not measured anywhere near the ground.)

The thermal capacity of the ground is also why, if you're insulating a basement or slab, you want to insulate the edges and DOWN - but you don't need to insulate in the center of the slab (as eventually the ground reaches the ambient temperature and acts as a huge thermal mass).
Probably not even rounding, but just printf. The following works on my zsh:

  $ printf '%.0f°\n' 0.3
  0°
  $ printf '%.0f°\n' -0.3
  -0°
That actually is rounding around zero (to zero decimal places, but it is rounding).
You're right. What I meant to say was that it's possible the programmers working on the weather app didn't add explicit rounding, they just passed the value through a formatter that did the rounding implicitly.
I believe that's actually a standard presentation, I've seen it in several weather graphs. It's basically a rounding yeah, signifying it's just slightly below zero, but not enough to round to -1°.
Maybe the actual temperature value has a few more digits that are truncated during formatting: "-0.001" -> "-0"
It’s pretty common when, for example, you’re computing the flows in a bifurcating field.

Take 2d laminar flow around a circle. The flow field splits right in the middle.

Signed zeros ensure that, along with graceful underflow, the local solution is not wonky.

Lots of complex-arithmetic examples too.

0 does double duty as meaning itself and to indicate underflow due to multiplying very small numbers.

If you have a function that can experience underflow it can be useful to preserve the sign of the underflowing value.

Otherwise you'd need more checks to get that information.

> Does anyone have a good example of a programming problem where we _need_ signed zero? Or where it makes things significantly simpler?

Maybe when you're implementing floating point numbers in a way that's simple and widely applicable?

I'm only half joking, too, though I can't tell you what exactly having a distinct sign bit simplifies.

I can say off the bat that it is probably useful that a very small quantity that might otherwise lose enough precision to round to zero maintains its sign regardless.

Exactly for things like OP’s argument: 1/-0 is -inf, and that may be important in asymptotics
A distinction can be made when considering the limit of a sequence. If a sequence converges to zero from positive values (sometimes written -> 0+), then the inverse of the sequence will diverge to +inf, while if the sequence converges to zero from negative values (-> 0-), the inverse will diverge to -inf.

As a sibling comment wrote, rounding numbers to -0 and +0 can provide extra information, though it may not be useful in all contexts.