2

I wrote a piece of code and came across a really strange problem. A comparison between two floats returns NO even when the actual comparison is true. I even used safe floating point comparison by comparing to FLT_EPSILON. This is the code:

//To start the process run this:
[self increment:0.0f];




- (void)increment:(float)f {
    f += 0.02f;

    if ((fabs(f - 1.0f) < FLT_EPSILON)) {
        NSLog(@"STOP");
    }
    else {
        NSLog(@"F %f", f);
        [self increment:f];
    }
}

And the comparison will always fail and the code it will go into an infinite loop. I have tested this on a 32 bit device on iOS 7 and on the iPhone 5S simulator on iOS 8.

JonasG
  • 9,274
  • 12
  • 59
  • 88

2 Answers2

3

The problem is that you are accumulating values which are not precise. FLT_EPSILON should be defined as the smallest value such that 1.0f + FLT_EPSILON != 1.0f.

What happens is that on each step you are adding a finite precision value to another finite precision value and you accumulate a small error. Since you are checking exactly for a value which is enough near to 1.0f to be undistinguishable from 1.0f then the check always fails.

If you need to stop at 1.0 you should instead check for if (f > 1.0f) directly, or use a looser constraint. Mind that using f > 1.0f could yield an additional iteration if the values comes a little bit before the desired one so it could be not suitable if the amount of iterations must be precise. Something like f > 1.0 - 0.02f/2 should be more precise.

0.98            1.0-0.02/2              1.0
 |                 |      ACCEPTABLE     |   ACCEPTABLE...   

lldb on Xcode 5.1

(lldb) p f
(float) $0 = 0.999999582
(lldb) p -(f - 1.0f)
(float) $1 = 0.000000417232513
(lldb) p __FLT_EPSILON__
(float) $2 = 0.00000011920929
(lldb) p (-(f - 1.0)) < __FLT_EPSILON__
(bool) $3 = false
Jack
  • 131,802
  • 30
  • 241
  • 343
  • This makes sense! Checking if f is greater than 1 works, and I've checked the value with lldb and it prints 1.01999962 on my setup. Thanks! – JonasG Aug 29 '14 at 07:36
  • Mind that checking if f > 1.0f implies an additional iteration in a situation in which the accumulated error gives a value which is smaller than the desired one (eg. on my output 0.999999582 should be 1.0 but with > 1.0f you realize it just on the additional iteration). Another solution could be to check for `f > MAX - INCREMENT/2` so that you accept a solution which is at nearer to the desired value more than to the previous one. – Jack Aug 29 '14 at 07:39
  • Yeah checking if f > 1.0f didn't always work out for me actually. A solution would be to use roundf for whole floats but then I could just use integers and scale the values up by 100 so that I get rid of these issues altogether. Thanks again for the help, great answer! – JonasG Aug 29 '14 at 07:45
  • More accurately, `FLT_EPSILON` should be defined as the distance between `1.0f` and its successor. Great answer nonetheless. http://blog.frama-c.com/index.php?post/2013/05/09/FLT_EPSILON – Pascal Cuoq Aug 29 '14 at 09:21
1

Must you use floats to solve the problem? An alternative would be to scale up to an int. If you need to use the float value, divide the int by 100.

- (void)increment:(NSUInteger)f {
    f += 2;

    if (f > 200) {
        NSLog(@"STOP");
    }
    else {
        [self increment:f];
    }
}
Pétur Ingi Egilsson
  • 4,368
  • 5
  • 44
  • 72