

Section 29:

[29.18] Why is cos(x) != cos(y) even though x == y? (Or sine or tangent or log or just about any other floating point computation)
I know it's hard to accept, but floating point arithmetic simply does not work like most people expect. Worse, some of the differences are dependent on the details of your particular computer's floating point hardware and/or the optimization settings you use on your particular compiler. You might not like that, but it's the way it is. The only way to "get it" is to set aside your assumptions about how things ought to behave and accept things as they actually do behave. Let's work a simple example. Turns out that on some installations, cos(x) != cos(y) even though x == y. That's not a typo; read it again if you're not shocked: the cosine of something can be unequal to the cosine of the same thing. (Or the sine, or the tangent, or the log, or just about any other floating point computation.) #include <iostream> #include <cmath> void foo(double x, double y) { if (std::cos(x) != std::cos(y)) { std::cout << "Huh?!?\n"; ← you might end up here when x == y!! } } int main() { foo(1.0, 1.0); return 0; }On many (not all) computers, you will end up in the if block even when x == y. If that doesn't shock you, you're asleep; read it again. If you want, try it on your particular computer. Some of you will end up in the if block, some will not, and for some it will depend on the details of your particular compiler or options or hardware or the phase of the moon. Why, you ask, can that happen? Good question; thanks for asking. Here's the answer (with emphasis on the word "often"; the behavior depends on your hardware, compiler, etc.): floating point calculations and comparisons are often performed by special hardware that often contain special registers, and those registers often have more bits than a double. That means that intermediate floating point computations often have more bits than sizeof(double), and when a floating point value is written to RAM, it often gets truncated, often losing some bits of precision. Said another way, intermediate calculations are often more precise (have more bits) than when those same values get stored into RAM. Think of it this way: storing a floating point result into RAM requires some bits to get discarded, so comparing a (truncated) value in RAM with an (untruncated) value within a floatingpoint register might not do what you expect. Suppose your code computes cos(x), then truncates that result and stores it into a temporary variable, say tmp. It might then compute cos(y), and (drum roll please) compare the untruncated result of cos(y) with tmp, that is, with the truncated result of cos(x). Expressed in an imaginary assembly language, the expression cos(x) != cos(y) might get compiled into this: ;Imaginary assembly language fp_load x ;load a floatingpoint register with the value of parameter x call _cos ;call cos(double), using the floating point register for param and result fp_store tmp ;truncate the floatingpoint result and store into temporary local var, tmp fp_load y ;load a floatingpoint register with the value of parameter y call _cos ;call cos(double), using the floating point register for param ans result fp_cmp tmp ;compare the untruncated result (in the register) with the truncated value in tmp ...Did you catch that? Your particular installation might store the result of one of the cos() calls out into RAM, truncating it in the process, then later compare that truncated value with the untruncated result of the second cos() call. Depending on lots of details, those two values might not be equal. It gets worse; better sit down. Turns out that the behavior can depend on how many instructions are between the cos() calls and the != comparison. In other words, if you put cos(x) and cos(y) into locals, then later compare those variables, the result of the comparison can depend on exactly what, if anything, your code does after storing the results into locals and comparing the variables. Gulp. void foo(double x, double y) { double cos_x = cos(x); double cos_y = cos(y); ... ← the behavior might depend on what's in here if (cos_x != cos_y) { std::cout << "Huh?!?\n"; ← you might end up here when x == y!! } }Your mouth should be hanging open by now. If not, you either learned pretty quickly from the above or you are still asleep. Read it again. When x == y, you can still end up in the if block depending on, among other things, how much code is in the ... line. Wow. Reason: if the compiler can prove that you're not messing with any floating point registers in the ... line, it might not actually store cos(y) into cos_y, instead leaving it in the register and comparing the untruncated register with the truncated variable cos_x. In this case, you might end up in the if block. But if you call a function between the two lines, such as printing one or both variables, or if you do something else that messes with the floating point registers, the compiler will (might) need to store the result of cos(y) into variable cos_y, after which it will be comparing two truncated values. In that case you won't end up in the if block. If you didn't hear anything else in this whole discussion, just remember this: floating point comparisons are tricky and subtle and fraught with danger. Be careful. The way floating point actually works is different from the way most programmers tend to think it ought to work. If you intend to use floating point, you need to learn how it actually works. 