Floating-point math is inherently complex due to its inexact nature. This article delves into the intricacies of comparing floating-point numbers for equality, exploring common pitfalls and providing practical solutions.
The Problem with Direct Equality
Representing decimal values like 0.1 exactly in binary floating-point format is impossible. Limited precision and variations in operations can alter results, making direct equality comparisons unreliable. For example:
float f = 0.1f;
float sum = 0;
for (int i = 0; i < 10; ++i)
sum += f;
float product = f * 10;
printf("sum = %1.15f, mul = %1.15f, mul2 = %1.15fn", sum, product, f * 10);
This seemingly simple code yields three different results for calculating ‘one’:
sum=1.000000119209290, mul=1.000000000000000, mul2=1.000000014901161
This discrepancy arises from rounding errors during conversion and operations. The true values of 0.1 in different formats highlight this:
Number | Value |
---|---|
0.1 | 0.1 (exact) |
float(.1) | 0.100000001490116119384765625 |
double(.1) | 0.1000000000000000055511151231257827021181583404541015625 |
Epsilon Comparisons: A Relative Approach
Instead of direct equality, we can check if the difference between two floats falls within an acceptable error margin (epsilon):
bool isEqual = fabs(f1 - f2) <= epsilon;
However, choosing an appropriate epsilon is crucial. FLT_EPSILON
, while representing the difference between adjacent floats around 1.0, becomes inadequate for smaller or larger numbers.
Relative Epsilon Comparisons: Accounting for Magnitude
A more robust approach involves comparing the difference relative to the magnitude of the numbers:
bool AlmostEqualRelative(float A, float B, float maxRelDiff = FLT_EPSILON) {
float diff = fabs(A - B);
A = fabs(A);
B = fabs(B);
float largest = (B > A) ? B : A;
return (diff <= largest * maxRelDiff);
}
This method generally works well with maxRelDiff
set to FLT_EPSILON
or a small multiple thereof.
ULP Comparisons: Units in the Last Place
Leveraging the integer representation of floats, we can determine the distance between two numbers in terms of Units in the Last Place (ULPs):
bool AlmostEqualUlps(float A, float B, int maxUlpsDiff) {
//Implementation details handling sign differences and ULP calculation
}
A smaller ULP difference indicates closer proximity. This method offers a more intuitive understanding of floating-point error. However, it has limitations near zero and with special values like NaNs and infinities.
Addressing Zero and Catastrophic Cancellation
Relative comparisons fail near zero. Subtracting nearly equal numbers can yield a result significantly different from zero in terms of ULPs or relative epsilon. An absolute epsilon check before relative comparison can mitigate this:
bool AlmostEqualRelativeAndAbs(float A, float B, float maxDiff, float maxRelDiff = FLT_EPSILON) {
// Implementation using both absolute and relative epsilon checks
}
Furthermore, seemingly simple calculations like sin(pi)
can exhibit catastrophic cancellation. The result reflects the error in the floating-point representation of pi rather than a true trigonometric calculation.
Conclusion
Comparing floating-point numbers for equality requires careful consideration. Direct comparison is often flawed. Relative epsilon and ULP comparisons offer more robust alternatives, but require careful selection of thresholds and understanding of limitations near zero. Combining absolute and relative epsilon checks enhances reliability. Ultimately, understanding the nature of floating-point arithmetic and potential error sources is paramount for accurate comparisons. No single solution fits all scenarios; the best approach depends on the specific context and expected error magnitudes.