C Language Experience Discussion (Part 6): Floating Point Equality Check, Why 0.1 + 0.2 β‰  0.3

C Language Experience Discussion (Part 6): Floating Point Equality Check, Why 0.1 + 0.2 β‰  0.3?

πŸ“Œ Application Scenarios and Issues

Floating point numbers in computers use the IEEE 754 standard for binary representation, but many decimal fractions cannot be precisely represented as binary floating point numbers. This leads to seemingly simple floating point operations potentially producing small errors, making direct use of <span>==</span> to compare floating point numbers unreliable. This precision issue is particularly critical in fields sensitive to numerical accuracy, such as scientific computing, financial systems, and game development.

Common Application Scenarios:

  • Financial Calculations: Accurate calculation and comparison of monetary amounts
  • Scientific Computing: Numerical integration and solving in physical simulations
  • Game Development: Precise determination of character positions and collision detection
  • Loop Control: Using floating point numbers as loop termination conditions

πŸ‘‰ Simple Analogy: Floating point numbers are like using a limited number of decimal places to represent infinitely precise real numbers, similar to using 5 decimal places to represent Ο€ (3.14159…)β€”there will always be rounding errors. When you make precise comparisons based on these “approximate values”, the results are often unexpected.

The insidiousness of such errors lies in the fact that they may work correctly in simple test cases but produce completely erroneous results in complex calculations or boundary conditions.

⚠️ Error Code & Output Results

#include <stdio.h>
#include <math.h>

int main() {
    // Scenario 1: The classic 0.1 + 0.2 != 0.3 problem
    printf("=== Classic Floating Point Precision Problem ===\n");
    double a = 0.1;
    double b = 0.2;
    double c = 0.3;
    double sum = a + b;

    printf("a = %.17f\n", a);
    printf("b = %.17f\n", b);
    printf("c = %.17f\n", c);
    printf("a + b = %.17f\n", sum);

    // ❌ Incorrect floating point comparison
    if (sum == c) {
        printf("0.1 + 0.2 equals 0.3\n");
    } else {
        printf("0.1 + 0.2 does not equal 0.3!!!\n");
    }

    // Scenario 2: Floating point counter in a loop
    printf("\n=== Floating Point Loop Counter Problem ===\n");
    int count = 0;

    // ❌ Using floating point as loop condition
    for (double i = 0.0; i != 1.0; i += 0.1) {
        printf("i = %.17f\n", i);
        count++;
        
        // Prevent infinite loop
        if (count > 15) {
            printf("Loop exceeded 15 iterations, forced exit\n");
            break;
        }
    }
    printf("Loop executed %d times\n", count);

    // Scenario 3: Precision error in financial calculations
    printf("\n=== Financial Calculation Precision Problem ===\n");
    double price = 0.1;
    double quantity = 3;
    double expected_total = 0.3;
    double calculated_total = price * quantity;

    printf("Unit price: %.2f yuan\n", price);
    printf("Quantity: %.0f pieces\n", quantity);
    printf("Expected total price: %.2f yuan\n", expected_total);
    printf("Calculated total price: %.17f yuan\n", calculated_total);

    // ❌ Direct comparison may lead to erroneous business logic
    if (calculated_total == expected_total) {
        printf("Price calculation is correct\n");
    } else {
        printf("Price calculation has an error! It may affect the financial system\n");
    }

    // Scenario 4: Accumulation error demonstration
    printf("\n=== Accumulation Error Demonstration ===\n");
    double accumulator = 0.0;
    double increment = 0.1;
    int iterations = 10;

    printf("Adding %.1f, %d times:\n", increment, iterations);
    for (int i = 0; i < iterations; i++) {
        accumulator += increment;
        printf("Iteration %d: %.17f\n", i+1, accumulator);
    }

    double expected_result = iterations * increment;
    printf("Expected result: %.17f\n", expected_result);
    printf("Actual result: %.17f\n", accumulator);

    // ❌ Incorrect accumulation result check
    if (accumulator == expected_result) {
        printf("Accumulation calculation is accurate\n");
    } else {
        printf("Accumulation error: %.17f\n", accumulator - expected_result);
    }

    return 0;
}

πŸ“Œ Actual Output Results:

=== Classic Floating Point Precision Problem ===
a = 0.10000000000000001
b = 0.20000000000000001
c = 0.29999999999999999
a + b = 0.30000000000000004
0.1 + 0.2 does not equal 0.3!!!

=== Floating Point Loop Counter Problem ===
i = 0.00000000000000000
i = 0.10000000000000001
i = 0.20000000000000001
i = 0.30000000000000004
i = 0.40000000000000002
i = 0.50000000000000000
i = 0.59999999999999998
i = 0.69999999999999996
i = 0.79999999999999993
i = 0.89999999999999991
i = 0.99999999999999989
i = 1.09999999999999987
i = 1.19999999999999984
i = 1.29999999999999982
i = 1.39999999999999980
i = 1.49999999999999978
Loop exceeded 15 iterations, forced exit
Loop executed 16 times

=== Financial Calculation Precision Problem ===
Unit price: 0.10 yuan
Quantity: 3 pieces
Expected total price: 0.30 yuan
Calculated total price: 0.30000000000000004 yuan
Price calculation has an error! It may affect the financial system

=== Accumulation Error Demonstration ===
Adding 0.1, 10 times:
Iteration 1: 0.10000000000000001
Iteration 2: 0.20000000000000001
Iteration 3: 0.30000000000000004
Iteration 4: 0.40000000000000002
Iteration 5: 0.50000000000000000
Iteration 6: 0.59999999999999998
Iteration 7: 0.69999999999999996
Iteration 8: 0.79999999999999993
Iteration 9: 0.89999999999999991
Iteration 10: 0.99999999999999989
Expected result: 1.00000000000000000
Actual result: 0.99999999999999989
Accumulation error: -0.00000000000000011

πŸ“ Output Result Explanation:

  • Precision Issue: 0.1 and 0.2 cannot be precisely represented in binary, resulting in a sum that does not equal 0.3
  • Infinite Loop: The floating point counter can never precisely equal 1.0, causing the loop to not terminate correctly
  • Financial Error: Small precision errors in financial systems can lead to serious consequences
  • Accumulation Error: Repeated floating point operations cause errors to accumulate, leading to significant deviations

πŸ‘‰ Error Analysis:

  1. The decimal fraction 0.1 is an infinite repeating decimal in binary, making it impossible to represent precisely
  2. Rounding errors in floating point operations accumulate over multiple calculations
  3. Directly using <span>==</span> to compare floating point numbers ignores the limitations of representation precision
  4. Floating point comparisons in loop conditions can lead to infinite loops

βœ… Correct Code & Output Results

Code Function Description:The following code demonstrates the correct way to handle floating point comparisons and operations:

  • Scenario 1: Use epsilon tolerance for floating point comparisons
  • Scenario 2: Avoid using floating point equality checks in loops
  • Scenario 3: Use integers or specialized high-precision libraries for financial calculations
  • Scenario 4: Strategies to control accumulation errors
#include <stdio.h>
#include <math.h>
#include <float.h>

// Define tolerance for floating point comparison
#define EPSILON 1e-9

// Safe floating point equality comparison
int float_equal(double a, double b, double epsilon) {
    return fabs(a - b) < epsilon;
}

// Relative error comparison (suitable for large values)
int float_equal_relative(double a, double b, double rel_epsilon) {
    if (a == b) return 1;  // Handle infinity or exact equality

    double diff = fabs(a - b);
    double largest = fmax(fabs(a), fabs(b));

    return diff <= largest * rel_epsilon;
}

int main() {
    // Scenario 1: Correct floating point comparison
    printf("=== Correct Floating Point Comparison ===\n");
    double a = 0.1;
    double b = 0.2;
    double c = 0.3;
    double sum = a + b;

    printf("a = %.17f\n", a);
    printf("b = %.17f\n", b);
    printf("a + b = %.17f\n", sum);
    printf("c = %.17f\n", c);
    printf("Difference = %.17f\n", sum - c);

    // βœ… Use epsilon tolerance for comparison
    if (float_equal(sum, c, EPSILON)) {
        printf("Within tolerance, 0.1 + 0.2 equals 0.3\n");
    } else {
        printf("Even within tolerance, 0.1 + 0.2 does not equal 0.3\n");
    }

    // Scenario 2: Correct loop control
    printf("\n=== Correct Loop Control ===\n");
    int count = 0;

    // βœ… Use integer counter to control loop
    for (int i = 0; i <= 10; i++) {
        double value = i * 0.1;  // Calculate by multiplication instead of accumulation
        printf("Iteration %d: %.17f\n", i, value);
        count++;
    }
    printf("Loop correctly executed %d times\n", count);

    // Or use range comparison instead of exact equality
    printf("\nUsing range comparison in loop:\n");
    count = 0;
    for (double i = 0.0; i < 1.0 + EPSILON; i += 0.1) {  // βœ… Use < instead of !=
        printf("i = %.17f\n", i);
        count++;
        
        if (count > 12) break;  // Safety measure
    }
    printf("Range comparison loop executed %d times\n", count);

    // Scenario 3: Correct handling of financial calculations
    printf("\n=== Correct Handling of Financial Calculations ===\n");

    // Method 1: Use integers for unit representation
    int price_cents = 10;    // 10 cents
    int quantity = 3;
    int total_cents = price_cents * quantity;  // 30 cents

    printf("Method 1 - Integer Calculation:\n");
    printf("Unit price: %d cents\n", price_cents);
    printf("Quantity: %d pieces\n", quantity);
    printf("Total price: %d cents (%.2f yuan)\n", total_cents, total_cents / 100.0);

    // Method 2: Use specialized comparison function
    printf("\nMethod 2 - Tolerance Comparison:\n");
    double price = 0.1;
    double qty = 3.0;
    double calculated = price * qty;
    double expected = 0.3;

    printf("Calculated total price: %.17f\n", calculated);
    printf("Expected total price: %.17f\n", expected);

    if (float_equal(calculated, expected, 1e-10)) {
        printf("Within financial precision requirements, calculation result is correct\n");
    } else {
        printf("Exceeds financial precision requirements\n");
    }

    // Scenario 4: Control accumulation errors
    printf("\n=== Control Accumulation Errors ===\n");

    // Method 1: Recalculate instead of accumulate
    printf("Method 1 - Recalculation:\n");
    double base_value = 0.1;
    for (int i = 1; i <= 10; i++) {
        double result = i * base_value;  // βœ… Recalculate each time
        printf("Iteration %d: %.17f\n", i, result);
    }

    // Method 2: Error compensation algorithm (Kahan summation)
    printf("\nMethod 2 - Kahan Summation Algorithm:\n");
    double kahan_sum = 0.0;
    double compensation = 0.0;
    double increment = 0.1;

    for (int i = 0; i < 10; i++) {
        double y = increment - compensation;
        double temp = kahan_sum + y;
        compensation = (temp - kahan_sum) - y;
        kahan_sum = temp;
        
        printf("Iteration %d: %.17f (Compensation: %.17f)\n", i+1, kahan_sum, compensation);
    }

    // Scenario 5: Relative error comparison
    printf("\n=== Relative Error Comparison ===\n");
    double large_a = 1000000.1;
    double large_b = 1000000.2;
    double large_sum = large_a + large_b;
    double large_expected = 2000000.3;

    printf("Large value calculation:\n");
    printf("%.17f + %.17f = %.17f\n", large_a, large_b, large_sum);
    printf("Expected result: %.17f\n", large_expected);

    if (float_equal_relative(large_sum, large_expected, DBL_EPSILON * 10)) {
        printf("Within relative error range, large value calculation is correct\n");
    } else {
        printf("Large value calculation exceeds relative error range\n");
    }

    return 0;
}

πŸ“Œ Correct Output Results:

=== Correct Floating Point Comparison ===
a = 0.10000000000000001
b = 0.20000000000000001
a + b = 0.30000000000000004
c = 0.29999999999999999
Difference = 0.00000000000000006
Within tolerance, 0.1 + 0.2 equals 0.3

=== Correct Loop Control ===
Iteration 0: 0.00000000000000000
Iteration 1: 0.10000000000000001
Iteration 2: 0.20000000000000001
Iteration 3: 0.30000000000000004
Iteration 4: 0.40000000000000002
Iteration 5: 0.50000000000000000
Iteration 6: 0.59999999999999998
Iteration 7: 0.69999999999999996
Iteration 8: 0.79999999999999993
Iteration 9: 0.89999999999999991
Iteration 10: 0.99999999999999989
Loop correctly executed 11 times

Using range comparison in loop:
i = 0.00000000000000000
i = 0.10000000000000001
i = 0.20000000000000001
i = 0.30000000000000004
i = 0.40000000000000002
i = 0.50000000000000000
i = 0.59999999999999998
i = 0.69999999999999996
i = 0.79999999999999993
i = 0.89999999999999991
i = 0.99999999999999989
Range comparison loop executed 11 times

=== Correct Handling of Financial Calculations ===
Method 1 - Integer Calculation:
Unit price: 10 cents
Quantity: 3 pieces
Total price: 30 cents (0.30 yuan)

Method 2 - Tolerance Comparison:
Calculated total price: 0.30000000000000004
Expected total price: 0.29999999999999999
Within financial precision requirements, calculation result is correct

=== Control Accumulation Errors ===
Method 1 - Recalculation:
Iteration 1: 0.10000000000000001
Iteration 2: 0.20000000000000001
Iteration 3: 0.30000000000000004
Iteration 4: 0.40000000000000002
Iteration 5: 0.50000000000000000
Iteration 6: 0.59999999999999998
Iteration 7: 0.69999999999999996
Iteration 8: 0.79999999999999993
Iteration 9: 0.89999999999999991
Iteration 10: 0.99999999999999989

Method 2 - Kahan Summation Algorithm:
Iteration 1: 0.10000000000000000 (Compensation: 0.00000000000000001)
Iteration 2: 0.20000000000000000 (Compensation: 0.00000000000000001)
Iteration 3: 0.30000000000000000 (Compensation: -0.00000000000000004)
Iteration 4: 0.40000000000000000 (Compensation: 0.00000000000000002)
Iteration 5: 0.50000000000000000 (Compensation: 0.00000000000000000)
Iteration 6: 0.60000000000000000 (Compensation: -0.00000000000000002)
Iteration 7: 0.70000000000000000 (Compensation: -0.00000000000000004)
Iteration 8: 0.80000000000000000 (Compensation: -0.00000000000000007)
Iteration 9: 0.90000000000000000 (Compensation: -0.00000000000000009)
Iteration 10: 1.00000000000000000 (Compensation: -0.00000000000000011)

=== Relative Error Comparison ===
Large value calculation:
1000000.09999999998835847 + 1000000.20000000004656613 = 2000000.30000000003492460
Expected result: 2000000.29999999998835847
Within relative error range, large value calculation is correct

πŸ“ Output Result Explanation:

  • Tolerance Comparison Successful: Using epsilon tolerance, correctly determined that 0.1 + 0.2 equals 0.3 within precision range
  • Loop Correctly Terminated: Using integer counters and range comparisons avoided infinite loops
  • Financial Calculation Safe: Integer calculations are completely precise, and tolerance comparisons meet business needs
  • Error Controlled: Kahan algorithm significantly reduces accumulation errors

✨ Key Comparison Points:

  • Eliminated logical errors caused by floating point precision
  • Loops can terminate correctly, making program behavior predictable
  • Financial calculations meet business precision requirements

🌟 Best Practices

1. The Golden Rule of Floating Point Comparison (Highly Recommended)

Absolute Error vs Relative Error?

  • Absolute Error: Suitable for comparisons of small decimal values close to 0
  • Relative Error: Suitable for comparisons of large values or values of different magnitudes
#include <float.h>

// Combined comparison function: automatically selects the most suitable method
int float_compare(double a, double b) {
    // Handle special cases
    if (a == b) return 1;  // Includes cases where both are equal to infinity

    double diff = fabs(a - b);

    // If one is close to 0, use absolute error
    if (fabs(a) < DBL_EPSILON || fabs(b) < DBL_EPSILON) {
        return diff < DBL_EPSILON;
    }

    // Otherwise use relative error
    double largest = fmax(fabs(a), fabs(b));
    return diff <= largest * DBL_EPSILON * 10;
}

2. Special Handling for Financial and Currency Calculations

#include <stdint.h>

// Use integers to represent currency (in cents)
typedef int64_t Money;  // 64-bit integer, supports large calculations

Money dollars_to_cents(double dollars) {
    return (Money)(dollars * 100 + 0.5);  // Round to nearest
}

double cents_to_dollars(Money cents) {
    return cents / 100.0;
}

Money money_multiply(Money amount, double rate) {
    return (Money)(amount * rate + 0.5);  // Round to nearest
}

// Usage example
Money price = dollars_to_cents(0.1);   // 10 cents
Money total = money_multiply(price, 3); // 30 cents
printf("Total price: %.2f yuan\n", cents_to_dollars(total));

3. Precision Control in Scientific Computing

#include <math.h>

// Set tolerances for different precision levels
#define PRECISION_SINGLE   1e-6    // Single precision floating point
#define PRECISION_DOUBLE   1e-12   // Double precision floating point
#define PRECISION_EXTENDED 1e-15   // Extended precision

// Choose precision based on application scenario
int scientific_equal(double a, double b, int precision_level) {
    double epsilon;
    switch (precision_level) {
        case 1: epsilon = PRECISION_SINGLE; break;
        case 2: epsilon = PRECISION_DOUBLE; break;
        case 3: epsilon = PRECISION_EXTENDED; break;
        default: epsilon = DBL_EPSILON * 10; break;
    }

    return fabs(a - b) < epsilon;
}

4. Compiler Optimization and Math Library Configuration

# Keep mathematical precision during compilation
gcc -ffast-math your_file.c          # Fast but may not be precise
gcc -fno-fast-math your_file.c       # Maintain IEEE 754 precision
gcc -ffp-contract=off your_file.c    # Disable floating point contraction optimization

# Link high precision math libraries
gcc your_file.c -lm -lmpfr           # Use MPFR high precision library

# Set floating point exception handling
gcc -ftrapping-math your_file.c      # Enable floating point exceptions

πŸ—οΈ Extension: Defensive Programming Approach

Use of High Precision Calculation Libraries

#include <stdio.h>

// Use long double for higher precision (if supported)
long double precise_calculation(long double a, long double b) {
    return a + b;
}

// Check platform's floating point precision support
void check_float_precision() {
    printf("float: %d bits precision, range: %e to %e\n", 
           FLT_MANT_DIG, FLT_MIN, FLT_MAX);
    printf("double: %d bits precision, range: %e to %e\n", 
           DBL_MANT_DIG, DBL_MIN, DBL_MAX);
    printf("long double: %d bits precision, range: %Le to %Le\n", 
           LDBL_MANT_DIG, LDBL_MIN, LDBL_MAX);
}

Considerations for Numerical Stability

// Avoid precision loss when subtracting large numbers from small numbers
double stable_subtraction(double large, double small) {
    if (fabs(large) > fabs(small) * 1e6) {
        printf("Warning: Subtracting small number from large number may cause precision loss\n");
    }
    return large - small;
}

// Use mathematical identities to avoid precision issues
double stable_quadratic_formula(double a, double b, double c) {
    double discriminant = b * b - 4 * a * c;
    if (discriminant < 0) return NAN;

    double sqrt_d = sqrt(discriminant);

    // Avoid precision loss when subtracting close values
    if (b >= 0) {
        double x1 = (-b - sqrt_d) / (2 * a);
        double x2 = c / (a * x1);  // Use Vieta's theorem
        return x1;  // Return one of the roots
    } else {
        double x1 = (-b + sqrt_d) / (2 * a);
        double x2 = c / (a * x1);
        return x1;
    }
}

πŸ“‹ Summary Checklist

  • βœ… Never use == to compare floating point numbers: Use epsilon tolerance comparison
  • βœ… Use integers for financial calculations: Perform integer operations in cents or smaller units
  • βœ… Avoid floating point loop counters: Use integer counters or range comparisons
  • βœ… Control accumulation errors: Recalculate instead of accumulate, consider Kahan algorithm
  • βœ… Select appropriate precision: Choose absolute or relative error based on application scenario

⚑ One-Sentence Summary: The precision issues of floating point numbers are an inherent characteristic of numerical computation; understanding and accepting this “imperfection” is crucial for handling it correctly!

Next Article Preview: “C Language Experience Discussion (Part 7): Array Out of Bounds, The Number One Killer of Memory Safety”β€”a deep dive into array boundary checks and safe access strategies.

Leave a Comment