12

This is my source code:

#include <iostream>
#include <cmath>

using namespace std;

double up = 19.0 + (61.0/125.0);
double down = -32.0 - (2.0/3.0);
double rectangle = (up - down) * 8.0;

double f(double x) {
    return (pow(x, 4.0)/500.0) - (pow(x, 2.0)/200.0) - 0.012;
}

double g(double x) {
    return -(pow(x, 3.0)/30.0) + (x/20.0) + (1.0/6.0);
}

double area_upper(double x, double step) {
    return (((up - f(x)) + (up - f(x + step))) * step) / 2.0;
}

double area_lower(double x, double step) {
    return (((g(x) - down) + (g(x + step) - down)) * step) / 2.0;
}

double area(double x, double step) {
    return area_upper(x, step) + area_lower(x, step);
}

int main() {
    double current = 0, last = 0, step = 1.0;
    
    do {
        last = current;
        step /= 10.0;
        current = 0;
        
        for(double x = 2.0; x < 10.0; x += step) current += area(x, step);
        
        current = rectangle - current;
        current = round(current * 1000.0) / 1000.0;
        //cout << current << endl;
    } while(current != last);
    
    cout << current << endl;
    return 0;
}

What it does is calculating area between the curves. There is a loop in main() - its purpose is to calculate value as precise as it's possible within 3 decimal places.

It didn't work. For the sake of debugging, I added the line which is the only commented one. I wanted to know what's going on inside the loop.

//cout << current << endl;

When the line is there - when I uncomment it - everything works fine. When it's not - the loop seems to be infinite.

Holy Compiler, why?

It's not the matter of imprecise floating-point numbers, which I am aware of. Everything is finished within 4 repeats of the loop content when I'm outputting current value inside it.

Anonymouse
  • 385
  • 3
  • 16
  • 7
    Did you use a debugger and did you check for undefined behavior? Read a guide on floating point numbers and ditch `current != last`. It's extremly unlikely to be true. http://floating-point-gui.de/ – stefan Feb 25 '15 at 15:19
  • 1
    Floating point numbers are prone to rounding errors, and the errors are compounded when you do operations on them. So e.g. `2.65` is not *exactly* `2.65` but could be e.g. `2.649997` or something like that. Therefore you can't reliably compare floating point numbers for equality. – Some programmer dude Feb 25 '15 at 15:21
  • No, the only debugging I did is what I described. – Anonymouse Feb 25 '15 at 15:21
  • 4
    Intel? doubles are 64 bit in memory but 80 bit internal and the call to cout is probably causes the value to be written to memory, thus losing precision, thus making the following test evalute to true. I would say "learn to use a debugger" and "learn about floating point numbers" – Skizz Feb 25 '15 at 15:21
  • 1
    possible duplicate of [Is floating point math broken?](http://stackoverflow.com/questions/588004/is-floating-point-math-broken) (not actually duplicate, but it's most likely this source of error) – stefan Feb 25 '15 at 15:22
  • 2
    If it's not about the rounding, how come that if i replace `current != last` with `std::fabs(current - last) >= 1e-8` everything works fine? http://ideone.com/kUqhhf This is all about rounding. The compiler probably performs some optimizations which is why you get the infinite loop. It's not the compilers fault, it's yours. – stefan Feb 25 '15 at 15:28
  • everything works fine here, wether with or without cout – Guiroux Feb 25 '15 at 15:29
  • your program gave output 117.705 on my pc. and there is no infinite loop. – MD. Khairul Basar Feb 25 '15 at 15:31
  • 1
    @Guiroux & Khairul Basar: That's perfectly normal: Different system means different behavior of floating point precision. That's why these bugs are so terribly mean: It might blow up somewhere, but not necessarily on the computer where you test it. – stefan Feb 25 '15 at 15:33
  • Rounding or not: You compare `double` for equality, and that's doing it wrong. – DevSolar Feb 25 '15 at 15:34
  • See Monniaux's “The pitfalls of verifying floating-point computations” http://arxiv.org/abs/cs/0701192 for a description of the heisenbug problem with floating-point. – Pascal Cuoq Feb 25 '15 at 16:56

2 Answers2

25

@Skizz's comment gives the likely problem, but to elaborate:

Floating point math is tricky, and, in particular, rounding errors can often arise. A number such as 1/1000.0 (the results of your round call) can't be precisely represented in floating point.

A further complication is that there are tradeoffs between speed on the one hand and consistent, intuitive results on the other. For example, an Intel processor's FPU stores values in an 80-bit extended precision format, while a C/C++ double is typically 64 bits. For performance, a compiler may leave values in the FPU, as 80-bit temporaries, even though this can produce different results than what you'd get if you truncated them to 64 bits.

With your debug statement enabled, current is likely stored to memory, truncating it to 64 bits, which permits a direct comparison with last.

With the debug statement disabled, current is likely an 80-bit value stored in an FPU register, and thus it can never equal last, as long as last is a 64-bit value and they're both trying to store an inexact floating point representation of x/1000.0.

The solution is to use a floating point comparison with some allowed error (because a direct check for equality with floating point is almost never a good idea).

Further notes: I haven't looked at the assembly output to verify that this is the case; you can do this yourself if you want. I'm only able to reproduce the problem if I enable optimizations. You may be able to "fix" the bug by tweaking compiler flags to choose consistency over speed, but the correct solution is to use an inexact comparison instead of a direct check for equality.

Josh Kelley
  • 56,064
  • 19
  • 146
  • 246
  • Excellent answer. My solution: being aware in advance that I have to compare values up to 3 decimal points, I compared integers (rounded values multiplied by 1000). – Anonymouse Feb 25 '15 at 15:37
  • are you sure this is the issue ? like have you ran the program printing the two values being compared each iterations to be sure they are not equals ? because i would agree normally, but i feel that about convergence problem, this particular issu wouldnt appears, but you know, just asking – Guiroux Feb 25 '15 at 15:42
  • @Guiroux - Floating point inexactness, 80- versus 64-bit values, and comparison with relative error are all known complications with FP that would explain this problem, but no, I haven't run it in a debugger or looked at the assembler to verify. (As Anonymouse explains, adding print statements makes the value go away, so that particular test doesn't work.) – Josh Kelley Feb 25 '15 at 15:50
  • 1
    could it be that printing it actually force it to go memory and so solve the problem ? if this could happen, i seriously overestimated the capacity to program without having a lot of hardware knowledge (like the 80bit thing) and overstimated abstraction power of c++. I'm so curious about this, seriously, could anybody with vaste knowledge and time investigate this ? or redirect me ? I mean somebody that can actually reproduce this behaviour cuz i can't – Guiroux Feb 25 '15 at 15:54
  • 1
    @Guiroux - There are well-known guidelines for dealing with floating point ("Floating point is hard. Rounding errors exist. Don't do direct checks for equality.") that you need to know if you're working with FP, but these guidelines are adequate for avoiding problems like this. Having a lot of hardware knowledge or an understanding of C++ optimization certainly helps, and it can explain how things go wrong, but following the guidelines is often enough for things to go right. If you'd like to read more on FP, the Bruce Dawson article I linked is a good resource. – Josh Kelley Feb 25 '15 at 16:05
  • ok thx a lot :D still waiting for somebody to investigate this particular case though :-P – Guiroux Feb 25 '15 at 16:07
  • 1
    Just to catalogue some facts that tend to confirm this answer. The phenomenon is not seen in MSVS Express 2012 (Debug or release). It can be seen compiling on ideone.com. It can be made to go away there by replacing the `while(current!=last)` with the even worse ` while( std::memcmp( &current, &last, sizeof(double) )!=0 )` or an `int equal(double,double)` function but only if inlining is suppressed. No, I'm not advocating exact comparison of `double`. – Persixty Feb 25 '15 at 16:10
4

Instead of

while(current != last);

use something like:

while(fabs(current - last) > tolerence);

where tolerance could be a small number, such as 1.0e-6.

R Sahu
  • 204,454
  • 14
  • 159
  • 270