I think Higham's Accuracy and Stability of Numerical Algorithms addresses how one can analyze these types of problems. See Chapter 2, especially exercise 2.8.
In this answer I'd like to point out something that isn't really addressed in Higham's book (it doesn't seem to be very widely known, for that matter). If you are interested in proving properties of simple numerical algorithms such as these, you can use the power of modern SMT solvers (Satisfiability Modulo Theories), such as z3, using a package such as sbv in Haskell. This is somewhat easier than using pencil and paper.
Suppose I'm given that $0\leq x\leq y$, and I'd like to know if $z=(x+y)/2$ satisfies $x\leq z\leq y$. The following Haskell code
import Data.SBV
test1 :: (SFloat -> SFloat -> SFloat) -> Symbolic SBool
test1 fun =
do [x, y] <- sFloats ["x", "y"]
constrain $ bnot (isInfiniteFP x) &&& bnot (isInfiniteFP y)
constrain $ 0 .<= x &&& x .<= y
let z = fun x y
return $ x .<= z &&& z .<= y
test2 :: (SFloat -> SFloat -> SFloat) -> Symbolic SBool
test2 fun =
do [x, y] <- sFloats ["x", "y"]
constrain $ bnot (isInfiniteFP x) &&& bnot (isInfiniteFP y)
constrain $ x .<= y
let z = fun x y
return $ x .<= z &&& z .<= y
will let me do this automatically. Here test1 fun
is the proposition that $x \leq \mathit{fun}(x,y) \leq y$ for all finite floats $x,y$ with $0\leq x\leq y$.
λ> prove $ test1 (\x y -> (x + y) / 2)
Falsifiable. Counter-example:
x = 2.3089316e36 :: Float
y = 3.379786e38 :: Float
It overflows. Suppose I now take your other formula: $z=x/2+y/2$
λ> prove $ test1 (\x y -> x/2 + y/2)
Falsifiable. Counter-example:
x = 2.3509886e-38 :: Float
y = 2.3509886e-38 :: Float
Doesn't work (due to gradual underflow: $(x/2)\times2 \neq x$, which might be unintuitive due to all arithmetic being base-2).
Now try $z=x + (y-x)/2$:
λ> prove $ test1 (\x y -> x + (y-x)/2)
Q.E.D.
Works! The Q.E.D.
is a proof that the test1
property holds for all floats as defined above.
What about the same, but restricted to $x\leq y$ (instead of $0\leq x\leq y$)?
λ> prove $ test2 (\x y -> x + (y-x)/2)
Falsifiable. Counter-example:
x = -3.1300826e34 :: Float
y = 3.402721e38 :: Float
Okay, so if $y-x$ overflows, how about $z = x + (y/2-x/2)$?
λ> prove $ test2 (\x y -> x + (y/2 - x/2))
Q.E.D.
So it seems that among the formulas I've tried here, $x + (y/2 - x/2)$ seems to work (with a proof, too). The SMT solver approach seems to me a much quicker way of answering suspicions about simple floating-point formulas than going through floating-point error analysis with pencil and paper.
Finally, the goal of accuracy and stability is often at odds with the goal of performance. For performance, I don't really see how you can do better than $(x+y)/2$, especially since the compiler will still do the heavy lifting of translating this into machine instructions for you.
P.S. This is all with single-precision IEEE754 floating-point arithmetic. I checked $x \leq x + (y/2-x/2) \leq y$ with double-precision arithmetic (replace SFloat
with SDouble
), and it works too.
P.P.S. One thing to bear in mind when implementing this in code is that compiler flags like -ffast-math
(some forms of such flags are sometimes turned on by default in some common compilers) will not result in IEEE754 arithmetic, which will invalidate the above proofs. If you do use flags that enable, e.g., associative addition optimizations, then there's no point in doing anything other than $(x+y)/2$.
P.P.P.S. I got carried away a little looking only at simple algebraic expressions without conditionals. Don Hatch's formula is strictly better.