Java doubles and 0.0

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

Juliean said:
assert(A == B);

Oof. So it is like that in C too? How could i miss this all the time?

Worried about potential perf. costs to handle special cases i have looked up what the cmp instruction really does and found this:

Performs a comparison operation between arg1 and arg2. The comparison is performed by a (signed) subtraction of arg2 from arg1, the results of which can be called Temp. Temp is then discarded.

That's not what i have expected (simple bitwise comparison ignoring math).
But it explains the mystery.

Advertisement

JoeJ said:
Oof. So it is like that in C too? How could i miss this all the time?

Well, that's one point that I kind of missed in frobs post. While a lot of floating-point specifics are implementation-details, and subject to diverse optimization; almost all languages use some form of the IEEE-standard. Thus a lot of languages share those characteristics.

JoeJ said:
That's not what i have expected (simple bitwise comparison ignoring math).

Well, first of all you looked at the wrong instruction. Float-point has it's own, that's the one I posted (https://www.felixcloutier.com/x86/ucomiss).​ You cannot compare floats with the generic cmp instruction. I tried that once (didn't want to handle floating point separately), and it surprisingly works for a lot of cases, but breaks with negative numbers etc…

Second, any cmp-instruction works the same way, as you described, by performing some sort of subtraction. The reason why it's not just doing bitwise equality is that cmp is shared for all types of comparisons. So whether you write ==, ≠, ≥, ≤, it all starts with cmp. cmp sets a certain flag, that then carries the information of how those values relate to each other. Another instruction is then used to check those flags and determine if the condition of the operator is fulfilled. So in reality there will be different instructions executed depending on whether the operator result is used in a branch (https://www.felixcloutier.com/x86/jcc) or stored in a value. But it's still as fast as can get, I don't think that doing it otherwise would be any faster, otherwise there'd be a way.
Floating-point instructions pretty much work the same, just there are some different instructions depending on a few properties of floating-points like NaN-handling, etc…

Oh, i just took the cmp from Mikes disassembly, and was already wondering assuming it's integer.

But yeah, the flags remind me on assembly coding on C64.
I have never learned x86, because i always assumed the ancient instruction set might see a modern replacement soon.
Still waiting for that…

Btw, another mystery:

Until recently, i have always used if (x != x) for a NaN check. I knew that's bad practice, but it worked for my debugging needs.
Suddenly it does no longer work, and the test is always negative. Probably after some VS update, i have to use std::isfinite() etc.

I do not really care, but i still wonder about the reason here.

JoeJ said:
Oh, i just took the cmp from Mikes disassembly, and was already wondering assuming it's integer.

Yeah, mikes assembly didn't do any float-comparisons at all, as I commented - it probably saw the static comparison of two constants and threw it out of the window (or replaced it with a dummy int-comparison as it saw both values are equal).

JoeJ said:
I have never learned x86, because i always assumed the ancient instruction set might see a modern replacement soon.

Doesn't seem like it. x86_64 has been in place for a long time, but it's just a superset of x86. I mean, we do have different alternatives like RISC, but I cannot comment on how large-scale their support/use is.

JoeJ said:
Until recently, i have always used if (x != x) for a NaN check. I knew that's bad practice, but it worked for my debugging needs. Suddenly it does no longer work, and the test is always negative. Probably after some VS update, i have to use std::isfinite() etc.

Maybe, eigther a codegen-change (or even bug) in MSVC. Or perpaps some of the floating-point optimization settings changed? floating-point model could have some effect on what you describe. If it's not that, it'll probably harder to figure out. You'd need to get back to a version of your compiled code that had the old behaviour and compare both dissassemblies. But, if as you mentioned it doesn't bother you that much, it's not worth that.

Juliean said:
Doesn't seem like it. x86_64 has been in place for a long time, but it's just a superset of x86. I mean, we do have different alternatives like RISC, but I cannot comment on how large-scale their support/use is.

We can at least safely assume there are at least as much ARM CPUs around as x86, so the support is given, just not yet on every OS.

My problem with x86 is the bloat. Modern CPUs need to translate the x86 IS into an internal IS which is actually used. So it would be nice to get rid of that redundant translation layer.

But now that i write this i realize: Even if all CPU vendors would agree on a new IS standard, it would get outdated quickly again. And we have the same situation after some time. Likely an ancient standard is better than none. See the chaos on GPUs, which due to a lacking standard are still practically unavailable to general programming. That's much worse than some inefficiency.

Still, on the long run i expect x86 to die. Why should only two and a half companies be allowed to make x86 CPUs? And others, most notably NVidia, don't get a licence and are excluded from competition?
That's not fair or good, so a changing industry will settle on something open sourced on the long run. Currently Risc-V seems promising.

C++ doesn't run on the processor.

Java doesn't run on the processor.

Compiled C++ might run on the processor, or might run through some levels of intermediate languages, intermediate virtual machines, and physical machines until it hits actual metal.

Compiled Java might run on the processor, or might run through intermediate language directly on a processor, or a JVM, or other layers until it hits actual metal.

Also, the CPUs vary, they take the ‘big’ instructions, decode them into micro-ops, and process them uniquely to that processor.

All of these details about floating point math tend to follow the IEEE 754 standard, or have documented exceptions from the standard. There are multiple representations of zero which all compare equal to each other. There are also encodings which are non-zero which can compare equal to zero, such as subnormal numbers. Processors can support features like DAZ (Denormals Are Zero) so any value that is denormal also results in a comparison as zero, they are not the same as +0.0 or -0.0 and yet when compared against them, so something like the SSE instructions COMISD or CMPPD will contain a non-zero value yet still compare as though it were zero.

(As an aside, in some ways it is like null pointer constants. It can be surprising for some programmers to learn there are many different values on systems which can all be null pointer constants, and null pointers can be different sizes like the old PC memory models of near-pointers, far-pointers, and huge pointers that were 16-bit and 32-bit sizes, and yet all the various null pointer constants are guaranteed that they can all be compared to each other and to a zero pointer. There are some special values that can have many different representations but the language mandates they must be comparable in specific ways regardless of the implementation cost.)

Exactly which CPU instructions get used will vary based on what compilers you're using and what processor your targeting. Today's desktop processors have several different ways to run them, either in the FPU stack or with SIMD instructions, and they both behave differently. Regardless of the implementation details, they must follow the language mandate that they ultimately compare equal.

Compiler options can change it, for example, Java's -strictfp can enforce additional conversions and additional comparisons that might not otherwise be there. Faster floating point optimization can cause scenarios to be missed, such as subnormal numbers not being treated as equivalent to zero, or not.

At the C++ level or the Java level, +0.0 and -0.0 compare equal because both language standards declare that they do, regardless of the CPU, the architecture, the physical chip, or anything else. They do it because the language says to do it without regard for the complexity or how many CPU instructions it takes.

This topic is closed to new replies.

Advertisement