We can distinguish two main cases: either x and y are almost_equal
, or they aren't.
When x and y are almost equal (down to a few bits), x+y
is almost equal to 2*x
. It's likely that x
and y
have the same exponent, but it's also possible that their exponent differs by exactly one (e.g. 0.49999 and 0.500001).
When x and y are not even close, e.g. when they differ by more than a factor of 2, then almost_equal(x,y)
will return true anyway - the difference between the current and proposed implementations doesn't really matter.
So, what we're really worried about is the edge case - where x
and y
differ in about ulp
bits. So getting back to the definition of epsilon
: it differs from 1.0 in exactly 1 Unit in the Last Place (1 ulp). Differing in N ULP's means differing by ldexp(epsilon, N-1)
.
But here we need to look carefully at the direction of epsilon
. It's defined by the next bigger number: nextafter(1.0, 2.0)-1.0
. But that's twice the value of 1.0-nextafter(1.0, 0.0)
- the previous smaller number.
This is because the difference between adjacent floating-point numbers around x scales with approximately log(x), but it's a step function. At every power of 2, the step size changes also by a step of 2.
So, your proposed lower bound of approximately x
indeed works for the best case (near powers of 2) but can be off by a factor of 2. And x+y
, which is almost equal to 2*x
when it matters, also works for the worst-case inputs.
So you could just multiply your bound by 2, but it's already a bit slower due to the hidden conditional.