Java doubles and 0.0

Started by
15 comments, last by frob 3 months, 2 weeks ago

I tried this in Java:

   public static void main(String[] args)
   {

       if (-0.0 == 0.0) System.out.println("they're equal");

   }

It says "they're equal". Does anyone know why?

I thought positive zero and negative zero were two different numbers in "double".

Does it do two checks if you do a zero comparison with a double?

Thank you.

Mike C.http://www.coolgroups.com/zoomer/http://www.coolgroups.com/ez/
Advertisement

For pretty much everything that you try and fail, you can safely assume someone else tried and failed before, and wrote about it at the Internet.

First hit was https://stackoverflow.com/questions/39933792/how-do-i-check-for-a-negative-zero-in-java-8​ pretty likely there are more.

This one seems more on-topic https://stackoverflow.com/questions/24238293/0-0-and-0-0-in-java-ieee-754

mike74 said:
It says "they're equal". Does anyone know why?

Because the numeric standard for the language says so. Positive and negative zeros compare as equal, despite not being bitwise identical.

There are many rules about the encoding of floating point, and the standards specify what happens.

You can read the basis of it for Java 21 here, but this specific behavior has been the same since the beginning of the language. Look down a few paragraphs to read “Positive zero and negative zero compare equal, so the result of the expression 0.0==-0.0 is true and the result of 0.0>-0.0 is false. Other operations can distinguish positive and negative zero; for example, 1.0/0.0 has the value positive infinity, while the value of 1.0/-0.0 is negative infinity.

It is that way because the standard says so. They chose it because other standards also say so. This works back until you get to the level of either mathematicians or electrical engineers declaring that method works better in their numeric system.

mike74 said:
I thought positive zero and negative zero were two different numbers in “double”.

They are. They have different binary encodings. That doesn't mean they're not equivalent.

There are often many ways to represent things that are functionally equivalent. Perhaps as a parallel, 1000 is the same value as 10^3, while the representations are different the number they represent is the same.

There are many things in the language and the libraries where equality does not mean bitwise equivalence.

mike74 said:
Does it do two checks if you do a zero comparison with a double?

It might. That detail is abstracted away by your language.

You don't know what the underlying system is. You might be on a processor that doesn't have a floating point processor. You might have a processer that only has 32-bit floating point support and needs to do extra work for 64-bit double precision. You might be on a system that has 80-bit precision which needs to do extra work to clamp it down to 64-bit precision.

The work for the comparison might be a single CPU instruction. It might be a series of mixed CPU and FPU instructions. It might be a set of work to send work to a separate math co-processor on a separate chip and stall for the result. It might be several hundred instructions to run the operation without using any floating point hardware.

The entire point of the language abstraction is that you don't need to know or care what is going on under the hood.

@Alberth It doesn't appear to be doing two comparisons.

I tried this in C++ and looked at the disassembly:

   if (-0.0 == 0.0) cout << "equal";
00007FF65894026D  xor         eax,eax  
00007FF65894026F  cmp         eax,1  
00007FF658940272  je          main+57h (07FF658940287h)  
00007FF658940274  lea         rdx,[string "equal" (07FF658952BC4h)]  
00007FF65894027B  mov         rcx,qword ptr [__imp_std::cout (07FF658960508h)]  
00007FF658940282  call        std::operator<<<std::char_traits<char> > (07FF65893116Dh)  
Mike C.http://www.coolgroups.com/zoomer/http://www.coolgroups.com/ez/

mike74 said:
I tried this in C++ and looked at the disassembly: if (-0.0 == 0.0) cout <<

Probably the Java complier already converts your illegal -0 into a proper 0 at compile time.

JoeJ said:
Probably the Java complier already converts your illegal -0 into a proper 0 at compile time.

What do you mean with “illegal”? Nothing about -0.0f is illegal (https://en.wikipedia.org/wiki/Signed_zero),​ as defined by IEEE. Also, he is now showing c++ assembly, not java.

The actual problem @mike74 with your c++-code is, that you are comparing two constants. Even a non-optimized build is able to detected that the result of the comparison is pretty much static, and will not bother doing actual float-comparisons at runtime. If you want to see whats actually happening, you need to use a setup more akin to https://godbolt.org/z/qYcYdq855. Somehow I cannot get a -O3 to actually compile the foo-method on both clang and gcc (regardless of what values those methods return), but the debug-assembly should give some idea.

So the answer for x86_64 is that it will execute a

ucomiss %xmm1, %xmm0

Which will do comparisons based on the IEEE-format and account for all details as specified by that standard.

Juliean said:
What do you mean with “illegal”?

According to Frobs post Java treats -0 == 0, so it can replace all -0 with 0, and if it does the number -0 does not appear anywhere and using sloppy language i called it ‘illegal’. I should have used parentheses.

Not sure if that's true though. If we do 1 / 0 and 1 / -0 in Java, does it not give negative infinity for the latter? This could be a problem in some cases.

JoeJ said:
According to Frobs post Java treats -0 == 0, so it can replace all -0 with 0, and if it does the number -0 does not appear anywhere and using sloppy language i called it ‘illegal’. I should have used parentheses.

Yeah, I think you misunderstand what frob meant. -0.0 compares equal to 0.0; that doesn't meant that they are equal or interchangable. They have a different bit-pattern, and as you mentioned switching one for the other could alter the observable state of the program. Maybe Java would allow such a thing (but since it also uses a IEEE-standard I doubt it), but C++ especially (which is what the assembly that mike posted came from) does not allow such a thing*. That's true in general though - equality doesn't mean interchangability.

* there are certain float-optimization flags that allows changing observable behaviour by relaxing requirements imposed by floating-point logic or the standards

Juliean said:
That's true in general though - equality doesn't mean interchangability.

I see. That's interesting.
But feels more confusing than helpful. I'll stick to C(++). : )

JoeJ said:
But feels more confusing than helpful. I'll stick to C(++). : )

I mean, the behaviour doesn't really change there. I was mainly talking about C++ as well, since I don't know Java as much. Going back to the example you brough up:

#include <cassert>

int main()

{

constexpr float A = 0.0f;

constexpr float B = -0.0f;

assert(A == B);

assert((1 / A) == (1 / B));

}


Oh great, the forums editor got fucked up even more, yay (I can't pre-select “code” and paste code into it, it will just paste it as plain-text, and if I wrap it as code after I'd have to remove the additional newlines -.-).

Aside from that, assertion 2 will fire. If A==B meant “whereever A is used, B can be used instead”, that would not be true. It also makes sense if you think about operator== not in the case of a compiler-managed intrinsic or numeric value, but in a general sense. You can easily implement operator== for a custom class to compare different objects. Does ObjectA == ObjectB mean that we can always use ObjectB or ObjectA interchangably? No. C++23 allows us to express this more with std::strong_ordering (which imposes that objects that compare equal are indistinquishable), but just from a logical baseline, this would not be the case otherwise.

This topic is closed to new replies.

Advertisement