Floating-Point Equality Isn't the Problem โ Your Epsilon Is
The debate over floating-point equality comparisons has quietly shaped how millions of developers write code โ and according to one veteran programmer with 15+ years in graphics, physics, and simulation work, the conventional wisdom is badly wrong.
Most developers have been taught a near-religious rule: never compare floating-point numbers for exact equality. Always use an epsilon. It's the kind of advice that gets passed down from senior engineers to juniors like a sacred commandment. But a recent post by lisyarus makes a compelling, technically rigorous case that this mantra is not just oversimplified โ it's actively harmful in most real-world scenarios.
This matters beyond academic computer science. As AI systems increasingly run on floating-point arithmetic at massive scale โ from GPU-accelerated neural networks to real-time medical imaging software โ the assumptions baked into how developers handle numerical precision have direct consequences for system reliability, debugging costs, and yes, patient safety.
The Epsilon Myth: Where It Comes From
The epsilon-comparison pattern looks innocent enough:
bool approxEqual(float x, float y) {
return abs(x - y) < 1e-4f;
}
The intuition behind it is understandable. Floating-point numbers can't represent every real number โ no finite memory system can, as the author notes, because "there are just way too many real numbers." A 32-bit float gives you at most 2ยณยฒ distinct representable values. Everything else gets rounded.
But here's the critical distinction the article draws, and it's one that most tutorials gloss over:
"This 'inexactness' of floating-point numbers doesn't mean some 'uncertainty' or 'randomness' in its behavior!"
Floating-point arithmetic under IEEE 754 โ the standard that governs virtually every modern CPU and GPU โ is deterministic. Any single arithmetic operation is required to produce the floating-point number closest to the true mathematical result. That's a guarantee, not a hope. The result is approximate, yes, but it's the best possible approximation, and it's the same every time you run the same operation on the same hardware with the same compiler settings.
This is the foundational insight that the epsilon-everywhere culture misses.
Three Reasons Epsilon Comparisons Break Things
The author lays out three serious structural problems with epsilon-based comparisons that go beyond theoretical nitpicking.
1. What Is the "Right" Epsilon?
There is no universal correct epsilon. The choice of 1e-4f versus 1e-6f versus 1e-9f is almost always a guess. And when different parts of a codebase make different guesses โ which they invariably do in any sufficiently large system โ the invariants that algorithms depend on silently break.
The author describes a scenario that will feel painfully familiar to anyone who has debugged geometry code:
"One part of the program treats 2D points as equal if they are separated in Manhattan distance by no more than 1e-4, another part of the program treats points as equal if they are separated in L-inf distance (max of coordinates) by no more than 1e-6, the input points themselves are generated using some other algorithm, and now all the input-output invariants are messed up, and your line rendering is broken but only in this specific scenario, with this specific data, only at night and in full moon."
That "only at night and in full moon" line is dark humor, but it captures something real: epsilon-induced bugs are extraordinarily hard to reproduce and isolate because they depend on specific data values hitting specific numerical edge cases.
2. Epsilon Comparisons Aren't Transitive
This is perhaps the most technically damaging problem. Mathematical equality is transitive: if A equals B and B equals C, then A equals C. Epsilon-based approximate equality is not transitive. You can easily construct three floating-point values where approxEqual(a, b) is true, approxEqual(b, c) is true, but approxEqual(a, c) is false.
Why does this matter? Because a staggering number of algorithms โ sorting, spatial indexing, graph traversal, collision detection โ implicitly assume transitivity in their comparison operations. Feed them a non-transitive comparator and you don't get slightly wrong results. You get crashes, infinite loops, or memory corruption.
3. Epsilon Comparisons Signal Misdiagnosed Problems
The deepest issue is that reaching for epsilon is usually a symptom of not having diagnosed why the comparison is uncertain in the first place. Is it because the input data is noisy? Because the algorithm accumulates rounding error? Because two independently computed paths should theoretically converge but don't? Each of these has a better solution than slapping an epsilon on the comparison.
So When Is Floating-Point Equality Actually Fine?
The author's core argument is that direct x == y comparison is appropriate โ and often preferable โ in many common situations that developers reflexively reach for epsilon to handle.
Checking Loop Termination and State Flags
If you're checking whether a value has been set to a specific sentinel (say, -1.0f to indicate "uninitialized"), exact equality is correct. The value was assigned that exact bit pattern; comparing it exactly is sound.
Verifying Idempotent Operations
If an operation is supposed to be idempotent โ applying it twice should give the same result as applying it once โ then exact equality is the right test. If f(f(x)) != f(x), that's a real bug, not floating-point noise.
Grid-Based or Discrete Systems
In a turn-based game where units move on a grid, movement points are often computed through simple arithmetic on values that were originally integers or simple fractions. The rounding behavior is predictable and consistent. Comparing with == 0.0f to check if a unit has no remaining movement is perfectly safe โ and much clearer than abs(movement_points) < 1e-5f.
After Exact Arithmetic Sequences
If you compute x = a + b and later check x == a + b again with identical inputs, you will get equality. Floating-point arithmetic is deterministic. The same inputs produce the same outputs.
The Bigger Picture: Why This Matters for AI Systems
Here's where my beat intersects with what might seem like a narrow programming debate.
The AI hardware boom โ European chip startups like Euclyd are currently seeking over $100 million in funding to compete with Nvidia's GPU dominance โ is fundamentally a story about floating-point compute at scale. Every transformer layer in a large language model, every convolution in a medical imaging classifier, every physics simulation in a robotics system runs on floating-point arithmetic.
The numerical precision assumptions baked into those systems matter enormously. The shift from FP32 to FP16 and now BF16 and FP8 training formats in AI is essentially a controlled, deliberate relaxation of floating-point precision in exchange for speed and memory efficiency. But that relaxation requires understanding what you're trading away โ not blindly applying epsilon comparisons and hoping for the best.
Consider the medical AI context. As I covered recently in AI Ultrasound Just Got FDA Clearance โ and the Real Battle Starts Now, AI diagnostic tools are now entering regulated clinical workflows. The numerical stability of the inference pipeline โ how activations, probabilities, and classification thresholds are computed and compared โ directly affects diagnostic outputs. A misunderstood epsilon in a post-processing step that rounds a confidence score to "positive" or "negative" isn't an academic concern. It's a patient safety issue.
And the stakes extend further. Reports that millions of Americans are already using AI chatbots for medical advice โ often receiving dangerously flawed guidance โ underscore how the gap between "technically functional" and "reliably correct" in AI systems has real human costs. Numerical hygiene in the underlying compute stack is one layer of that reliability problem, and it's one that developers control directly.
What Developers Should Actually Do
The article's prescription is essentially: think harder about the source of uncertainty before choosing a comparison strategy.
Here's a practical framework distilled from the argument:
Ask why the comparison might fail. Is it accumulated rounding error across many operations? Is it input data that arrives with measurement noise? Is it two algorithms that should theoretically converge but were implemented differently? Each answer points to a different solution.
Restructure the algorithm to eliminate the uncertain comparison. Often the comparison is only uncertain because the algorithm is computing the same thing two different ways. Refactoring to compute it once and store the result eliminates the discrepancy entirely โ no epsilon needed.
Use exact equality when the value has a known, exact provenance. If you assigned x = 1.0f, then x == 1.0f is correct and safe. If you computed x through a long chain of arithmetic, exact equality may not be appropriate โ but the solution is to understand that chain, not to guess an epsilon.
When approximate comparison is genuinely needed, use ULP-based comparison. Units in the Last Place (ULP) comparison measures how many representable floating-point values lie between two numbers, which scales naturally with magnitude. This is more principled than an arbitrary absolute epsilon. The IEEE 754 standard documentation provides the theoretical grounding for this approach.
Never use epsilon comparisons in algorithms that require transitivity. Sorting, set membership, spatial hashing โ if the algorithm's correctness depends on equivalence being transitive, epsilon-based equality will eventually corrupt it.
The Deeper Engineering Lesson
There's a broader principle here that extends well beyond floating-point arithmetic. Cargo-culted rules โ "always use epsilon," "never use raw pointers," "always use an ORM" โ spread because they encode real wisdom from specific contexts. The problem is that the context gets stripped away in transmission. Junior developers receive the rule without the reasoning, apply it universally, and create new categories of bugs that the original rule was never designed to prevent.
The author's 15+ years of experience in geometry, graphics, physics, and simulation represents exactly the kind of domain-specific depth that produces useful counter-intuitions. These fields live and die by numerical precision. The fact that experienced practitioners in these areas have largely moved away from epsilon comparisons โ not toward them โ should prompt developers in adjacent fields to examine their own assumptions.
This connects to a pattern I've noted in AI system design as well: the tools that AI systems use to decide what to run next often encode similar cargo-culted assumptions about numerical thresholds, confidence cutoffs, and similarity scores. When those assumptions are wrong โ or internally inconsistent โ the failures are opaque, data-dependent, and maddeningly hard to reproduce.
The Bottom Line
The conventional wisdom about floating-point equality is due for a serious audit. Not because floating-point arithmetic is simple or forgiving โ it isn't โ but because the epsilon-everywhere reflex substitutes a false sense of safety for genuine numerical understanding.
Direct x == y comparisons are correct and appropriate far more often than the conventional wisdom suggests. Epsilon comparisons introduce their own failure modes โ mismatched tolerances, broken transitivity, hidden invariant violations โ that are often worse than the problem they were meant to solve.
The right approach is to understand what you're computing, understand where rounding error enters the system, and choose a comparison strategy that matches the actual mathematical structure of the problem. That requires thinking, not pattern-matching to a rule. But in a world where floating-point arithmetic increasingly underpins medical diagnostics, autonomous systems, and financial infrastructure, the cost of not thinking is rising fast.
Alex Kim
Former financial wire reporter covering Asia-Pacific tech and finance. Now an independent columnist bridging East and West perspectives.
Related Posts
๋๊ธ
์์ง ๋๊ธ์ด ์์ต๋๋ค. ์ฒซ ๋๊ธ์ ๋จ๊ฒจ๋ณด์ธ์!