Learn Objective-C: Floating in Uncertainty

Among the primitive data types that Objective-C offers are float and double. Similar to long, double is simply double the storage in bits of float. However, as the name suggests, both are floating-point numbers. That is, the decimal point literally “floats” around as necessary. This can lead to many subtle bugs, even between executions of the same program.

How Do They Work?

Both types have a certain numbers of bits for storage. The exact number of bits varies per implementation, just as the exact storage sizes for ints and doubles aren’t fixed. However, a floating point number effectively has two parts, the “whole” part and the “fractional” part. The floating nature comes in because the number of bits used to store each section is not always the same, and will vary depending on the sizes of the numbers involved.

The Precise Issue

Although there are an infinite number of integers, they all differ by the same amount- 1. Therefore it is relatively easy to represent an integer value as bits. By contrast, however, the difference between one decimal number and another is incredibly small, and, what’s worse, this difference can change. When trying to squeeze an infinite number of digits with infinite precision into a finite number of bits, something has to give. This usually means that the floating-point value is rounded, and is not completely accurate. Here’s a simple program (using plain C) to illustrate this issue:

int main (int argc, const char *argv[]) {
    float f = 1234.123456789;
    printf("%f\n", f);
    printf("%.9f\n", f);
    return 0;
}

Output:

1234.123413
1234.123413086

The first print statement (printf() is nearly identical to NSLog(), except that it takes C-style strings instead) shows the actual value of f in memory, with ten digits in this case. Notice that the value is different from the value we assigned to the variable. This is because there are not enough bits to hold every single digit, so some of it had to be rounded away. That rounding is not perfectly accurate- it rounds the tailing ‘456789’ into ‘413’. If, however, we try to force it to display nine digits as seen in the next line, we still end up with the ‘413’, along with a tailing ‘086’- not digits that we put in.

Comparisons

How many times should this loop run?

#import <Foundation/Foundation.h>
int main (int argc, const char * argv[]){
    NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
    int count = 0;
    for (float i = 10; i != 0; i -= 0.1) {
        count++;
        printf("count is %d\n", count);
    }
    [pool drain];
    return 0;
}

Common sense tells us that it should run 100 times, but in fact this is an infinite loop. Because of these rounding errors, i will never be exactly equal to zero. It’ll come close after 100 cycles through, but it won’t be perfect. As such, when dealing with floating point numbers, you want to check to see if it’s close to a value. In the above example, the comparison should be i >= 0; (the equal bit because the floating point number might be a tiny negative value) to ensure that the loop ends.

To check if two floating point numbers are equal, you could use the == (double equals) operator, but there’s no guarantee that they will give an accurate result, especially after many calculations. A better way (albeit less efficient) would be to get the absolute value of their difference, and see if it’s less than a small number, such as 0.00001. If it’s less, you can safely assume they’re equal; otherwise, they’re not.

Floating point inaccuracies can be very difficult to debug, so it pays off to take precautions earlier. As a matter of fact, I spent about half an hour trying to figure out why something wasn’t showing up on screen, when in fact a botched conversion from integers to floating point (similar issues result) lead to everything being 0. Not fun.

This post is part of the Learn Objective-C in 24 Days course.


Previous Lesson Next Lesson