logo
Kurashizu Blogwhere ideas flow
  • Home
  • News
  • Blog
  • About
  • Home
  • News
  • Blog
  • About

© 2026 Kurashizu. All rights reserved.

Admin·New Post·Service Status
Powered by Cloudflare·30.10.26
← Back to news

Float and integer arithmetic follow two different paradigms

Original article
blog.pkh.me|48 points|10 comments|by ibobev|Oct 5, 2026

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

LanguageApproachMechanismResult of Failure
CManual/IntrinsicINT32_MAX checks or __builtin_mul_overflowUndefined Behavior / Crash
RustExplicit 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 ∞\infty∞ (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:

if (y == 0) { return error; } →\rightarrow→ This is often insufficient.

When developers realize that very small numbers can also cause issues, they introduce an arbitrary epsilon (ϵ\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 3.4×10383.4 \times 10^{38}3.4×1038. If we divide this by 0.90.90.9 (which is significantly larger than FLT_EPSILON), the result is still ∞\infty∞.

Similarly, if x=5×1031x = 5 \times 10^{31}x=5×1031 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)

Floating Point Logic


🛠️ 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:

  1. Resilience: It handles 0/00/00/0 or −1\sqrt{-1}−1​ automatically via NaN propagation.
  2. Simplicity: Complex formulas can be evaluated fully, with a single check at the end rather than dozens of intermediate guards.
  3. Accuracy: It avoids rejecting valid inputs that happen to fall near an arbitrary ϵ\epsilonϵ 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.

  • isfinite is missing.
  • isinf exists but ignores NaN.
  • isnan is often unreliable.

This makes the graphics stack a particularly treacherous environment for floating-point arithmetic.