Division without division and other computational tricks

What is this about? 🔗

You want to divide two numbers with your computer.

No problem: Just let your computer do it!

But in rare cases, your computer may not know how to divide.

This could be the case if said computer is a mere µcontroller. Even more so if you do your own arithmetic, e.g., BCD or fixed point.

So this post assumes your computer knows how to do addition +, subtraction -, multiplication *, and comparison with <, but it does not yet know division /. It offers a software solution that teaches your computer division.

I understand one normal expert advice for teaching computers division is to use the CORDIC algorithm family (which can even be used to teach your computer multiplication, in case it has not learned that yet, either). But that CORDIC stuff is quite a beast to wrap your head around (which I, honestly, have not really tried yet). Apparently, it also needs a bit of ROM space for lookup tables. Finally, from my present partial understanding, CORDIC seems to be intended for binary numeric representation; the approach offered here can be used for BCD numbers unchanged.

So this toot dares to suggest a quite simple home-grown approach to the division problem.

That approach has a reasonably decent runtime performance, though it may not be optimal. It results in short program code which is easy to get right. And it doesn’t need lookup tables.

And, of course: If you just want to read this for the fun of it, without any particular application in mind, you are warmly invited to do so.

Full disclosure: I wrote this for the fun of it, without any particular application in mind.

In a nutshell 🔗

You are given a number a>0 and desire to calculate 1a.

(That is actually all that is missing to do general division: Instead of dividing something by a, multiply it with 1a.)

To do so, start with any value x0 that is a reasonable approximation of 1a.

Here, the requirements for “reasonable” are lax. Any x0 with 0<x0<2a will work. (But the better your initial approximation is, the faster the calculation completes.)

From your start value x0, define a sequence x0,x1,x2, of better and better approximations for 1a via the simple formula

xn+1=xn(2-axn)

Here is a numerical example with a=3, using x0=0.5 as an initial approximation for 13.

value
​​x0 0.500000000000000000
x1 0.250000000000000000
x2 0.312500000000000000
​​x3 0.332031250000000000
​​x4 0.333328247070312500
​​x5 0.333333333255723119
​​x6 0.333333333333333315
​​x7 0.333333333333333315
​​x8 0.333333333333333315

The sequence stabilizes with all subsequent values being identically to x6. That value incidentally is the best that can be achieved, as the next higher value that can be represented by the floating point number scheme used is 0.333333333333333370, which is less precise.

In all numerical calculations presented here, I used whatever floating point number scheme my PC’s libraries offer. I believed this to be the ubiquitous IEEE 754 binary64, but I did nothing to verify this.

Once x2 has found the first correct digit, each of the few next subsequent values has twice as many correct digits as its predecessor. This is typical of the quadratic convergence afforded by the method presented here.

When to stop? 🔗

One might be tempted to stop when xn=xn+1. This will work most of the time, but not always. In rare cases, the sequence will oscillate.

One example found by brute-force search is a=338 with start value x0=0.002:

value
x0 0.0020000000000000000
x1 0.0026479999999999997
x2 0.0029259764480000002
x3 0.0029582205931032645
x4 0.0029585798380249708
x5 0.0029585798816568042
x6 0.0029585798816568051
x7 0.0029585798816568042
x8 0.0029585798816568051

The value returned by the build-in division in this case prints as 0.0029585798816568047.

It is conceivable that in some cases, the sequence might end up in a merry-go-round of three or even more numbers. With a brief brute-force search, I did not find any occurrences of that, but it should not be ruled out.

In the realm of pure mathematics and precise real numbers, the algorithm employed guarantees an increasing sequence x1<x2<x3<. (This statement purposefully omits the possibly too-high start-value x0.)

In the practical realm of computers with finite-precision arithmetic, I suggest as a straightforward termination criterion xn​​​≥xn+1. When that triggers, a good finite-precision approximation of the desired result 1a is given by xn+1.

For a posh twist, you can use that only if xn​​​=xn+1, and 0.5(xn+1+xn+2) if not. This results in a marginal increase in typical precision, at the expense of a marginal increase in runtime.

Choice of start value 🔗

One can try to find a good start value x0 by analyzing the given number a to be inverted.

Where floating point numbers are used, analysis of the representation of a yields information about the magnitude of a, from which a rough approximation of its inverse can be obtained that is good enough to serve as x0.

A more pedestrian approach starts with the value x=1 and calculates the product xa. If that product is close enough to 1, then x can be used as is. If not, x can be either increased or decreased as pertinent, and xa calculated anew.

This pedestrian approach is used in the following sample implementation:

Sample implementation 🔗

Here is a sample implementation for this algorithm in Python:

def sample_implementation(a):
    assert 0 < a

    # Find decent start value x
    # between 0.17/a and 1.8/a:
    x = 1
    xa = x * a
    while 1.8 < xa:
        x *= 0.1
        xa = x * a
    while xa < 0.17:
        x *= 10
        xa = x * a
    # The multiplication factors
    # of 0.1 and 10 in the above
    # have been pulled
    # out of thin air.

    # They'll work, but better
    # choices may be possible.

    # If representation is binary,
    # using the values 0.125 and 8
    # will most likely
    # speed up calculations.

    # Omit the possibly too high
    # start value x, proceed to the
    # next value in the sequence
    # right away:
    x = x * (2 - xa)

    while True:
        x_new = x * (2 - x * a)
        if x_new <= x:
            return x_new
        x = x_new

To verify, the results of this implementation where compared with that of the system’s floating point division for 1,000,000 values in the range 1e-300 through 1e+300.

The results from this implementation where either the same as that computed by the system’s 1/a, or it was the next lower floating point value that can be represented, or the next higher. In no case was the result from this implementation further off the system’s, it was always one of those three.

In percentages:

what frequency
same value 67.8 %
next lower value 23.2 %
next larger value 9.0 %

For a further experiment, the posh implementation of calculating the return value was tried:

if x_new <= x:
    if x_new == x:
        return x_new
    else:
        return 0.5 * (
          x_new +
          x_new * (2 - x_new * a)
        )

This was only slightly better:

what frequency
same value 68.1 %
next lower value 22.9 %
next larger value 9.0 %

Suggested exercises 🔗

The math behind this: a theorem. 🔗

Theorem’s assumptions 🔗

Assume a real-valued function f is defined over the open non-empty interval of real numbers between two numbers u and v and is twice continuously differentiable over that interval.

Assume a point z exists in that interval, that is, u<z<v, which is a fixpoint of f, that is, f(z)=z.

Assume further f(z)=0, and also 0f(x) for all x such that u<xz, and finally, assume that no fixpoint of f exists larger than u and smaller than z.

Theorem’s claims 🔗

Assuming that first claim to be true, given any x0 such that u<x0z, we can define a sequence recursively via xn+1=f(xn) for all indices (natural numbers) n.

This sequence has the following properties:

Application 🔗

Assuming for a moment the theorem given were true. It can be applied to our inversion problem as follows:

Suggested exercise 🔗

Convince yourself (in a “pedestrian” way) that for any x with 0<x<1a the assertion x<f(x)z holds in our particular 1a situation.

Suggested exercise 🔗

Convince yourself that the theorem allows the use of the function f(x)=x+sin(x) to calculate π.

(There are much more convenient ways to calculate π. But this way is at least legitimate, as the sin function can be calculated via its power series without prior knowledge of π.)

Suggested exercise 🔗

In music, the “equal temperament” demands that the quotient of the frequencies of two adjacent (half-)tones is z=212.

Proof of the theorem 🔗

We first need to prove: Given x such that u<x<z, it is true that x<f(x)z.

For f(x)z: According to the mean value theorem, there must exist some ξ with x<ξ<z such that f(ξ)=f(z)-f(x)z-x. According to our assumptions, f(ξ)0. As x<z, we see f(x)f(z)=z.

For x<f(x), consider the function p(x)=f(x)-x. This function is zero precisely where f has a fixpoint. In particular, p(z)=0. Now f has no fixpoint between u and z (exclusively), so p becomes zero nowhere in that interval. Being a continuous function, p cannot change sign over an interval without assuming a zero value in that interval. So p cannot change sign between u and z (exclusively), in other words, must either be always negative or always positive there. Now p is continuously differentiable. Its derivative evaluates to -1 at z. As the derivative is continuous, it must be negative for some interval of points slightly smaller than z. By another application of the mean value theorem, p must be positive on that same interval. We established p does not change sign in the interval strictly between u and z, and we have found points in that interval where p is positive, so we now know it must be positive everywhere between u and z (exclusively), which in particular proves p(x)>0. Now p(x)>0 is equivalent to x<f(x).

These two together prove that the sequence x0,x1,x2, increases and is bounded from above by z.

Any increasing bounded sequence of real numbers converges. (How to prove this fact, which is both profound and basic, depends on your definition of real numbers.)

Let us call the limit ζ=limnxn. As f is continuous:

f(ζ)=f(limnxn)=limnf(xn)=limnxn+1=limnxn=ζ

So ζ is a fixpoint of f, which forces ζ=z. So the sequence x0,x1,x2, converges with limit z.

The only claim not yet proven is that this convergence is quadratic.

This is a direct consequence of Taylor’s theorem. It allows us to rewrite f(xn)=f(z)+f(z)(xn-z)+R1(xn)

In our case, f(z)=0, so this reduces to f(xn)=f(z)+R1(xn) or f(xn)-f(z)=R1(xn) or xn+1-z=R1(xn). The Lagrange form of the remainder boils down to R1(xn)=f(ξ)2(xn-z)2 for some suitable value ξ with xnξz. Now f is continuous, hence bounded over any closed interval containing z. If we have an upper bound k of |f| over some such interval and let n become large enough so all subsequent xn are contained in that interval, then indeed |xn+1-z|k2|xn-z|2, which completes the proof.

Suggested exercise 🔗

Formulate the mirror-image theorem, where convergence ends up not being from below, but from above, and prove it.

Use your theorem to prove that Heron’s method works, which uses the recursion formula xn+1=12(xn+axn) to calculate a.

Use your theorem to prove that recursion via the function f(x)=x+sin(x) converges to π for any start value x0 with πx0<2π.

Suggested exercise 🔗

Above, there is a definition of “quadratic convergence” that loosely means that after a certain number of digits of the final result have been calculated correctly, the next iteration step tends to produce twice as many correct digits. There are approximation sequences that exhibit cubic convergence: The next iteration step tends to produce three times as many correct digits as the previous one has established.

Author’s remark 🔗

This was also an experiment regarding typing math in via raw mathml. Result: Can be done, but is not entirely convenient.

I did not bother installing web fonts, but rely on your browser’s support of mathml. Depending what browser you use, your mileage may vary.

Discussion opportunity 🔗

If you want to comment or discuss this piece and have a Fediverse account, feel invited to answer my pertinent toot.