Float and integer arithmetic follow two different paradigms
The Divergent Paradigms of Integer and Floating-Point Arithmetic
Thesis: When developers handle floating-point numbers, they frequently default to patterns used in integer arithmetic. This is a fundamental category error.
I have observed this recurring mistake repeatedly. The fact that Large Language Models (LLMs) consistently propagate this error suggests that either my understanding is flawed, or the collective consensus is. I am confident it is the latter.
🛡️ The Integer Arithmetic Safety Model
In the realm of integers, the primary goal is to prevent a catastrophe before it occurs.
As I have previously discussed, attempting to detect an integer overflow after it has happened is often futile. Because C compilers assume the programmer writes safe code, they may optimize away any checks that occur after an undefined behavior (UB) event.
Language Comparison: Integer Overflow Handling
| Language | Approach | Mechanism | Result of Failure |
|---|---|---|---|
| C | Manual/Intrinsic | INT32_MAX checks or __builtin_mul_overflow | Undefined Behavior / Crash |
| Rust | Explicit Methods | .checked_add(), .saturating_sub(), etc. | Runtime Panic (in debug) |
To ensure safety in C, developers must perform "computational gymnastics." With the advent of C23, we finally have stdckdint.h providing ckd_* helpers, but the burden remains on the developer to anticipate failure.
Developer's Safety Checklist for Integers:
- Anticipate potential overflows.
- Use compiler built-ins for multiplication/addition.
- Use saturating logic where appropriate.
- Avoid post-facto error detection.
🌊 The Floating-Point Paradigm (IEEE-754)
Floating-point types operate under an entirely different philosophy. Instead of crashing or triggering UB, operation errors result in special values: NaN (Not a Number) or (Infinity). These values are legitimate and propagate through subsequent calculations.
The "Epsilon" Trap
Despite this, developers often try to apply integer-style "guards." A common sight is the check for a zero denominator:
This is often insufficient.if (y == 0) { return error; }
When developers realize that very small numbers can also cause issues, they introduce an arbitrary epsilon ():
if (fabs(y) < FLT_EPSILON) { ... }
Why this fails: The validity of a division depends on the relative magnitude of both the numerator and the denominator, not just the denominator.
Mathematical Proof of Failure
Consider the maximum 32-bit float, roughly . If we divide this by (which is significantly larger than FLT_EPSILON), the result is still .
Similarly, if and we divide by the smallest representable float above FLT_EPSILON, we still hit infinity.
fn main() {
let max = f32::MAX;
let eps_next = f32::EPSILON.next_up();
let r0 = max / 0.9_f32;
let r1 = 5e31 / eps_next;
println!("{:e}/0.9={:e} (inf:{})", max, r0, r0.is_infinite());
println!("5e31/{:e}={:e} (inf:{})", eps_next, r1, r1.is_infinite());
}
// Output:
// 3.4028235e38/0.9=inf (inf:true)
// 5e31/1.192093e-7=inf (inf:true)
🛠️ The Correct Approach: Post-Calculation Validation
Instead of guessing thresholds, we should simply execute the operation and verify if the result is a finite number using isfinite (C) or is_finite (Rust).
Implementation Example (C)
#include <math.h>
int my_div(float x, float y, float *r) {
*r = x / y;
return isfinite(*r); // Single check for both NaN and Inf
}
Note: This assumes an IEEE-754 compliant environment.
Why this is superior:
- Resilience: It handles or automatically via
NaNpropagation. - Simplicity: Complex formulas can be evaluated fully, with a single check at the end rather than dozens of intermediate guards.
- Accuracy: It avoids rejecting valid inputs that happen to fall near an arbitrary threshold.
⚠️ Important Caveats
1. Numerical Instability
isfinite ensures the number is representable, but it does not ensure the number is correct. Numerical instability can produce "finite garbage."
fn main() {
let a = 100_000_000_f32;
let b = 100_000_000_f32;
let c = 1_f32;
let x = a + c - b; // Mathematically should be 1.0
println!("{} (finite:{})", x, x.is_finite());
}
// Output: 0 (finite:true)
2. The GLSL Nightmare
In graphics programming (GLSL), the rules are looser. Unless highp precision is enabled via GL_FRAGMENT_PRECISION_HIGH, NaN might not even exist. Even then, IEEE-754 propagation isn't strictly required.
isfiniteis missing.isinfexists but ignoresNaN.isnanis often unreliable.
This makes the graphics stack a particularly treacherous environment for floating-point arithmetic.