34

I've got a fixed point class (10.22) and I have a need of a pow, a sqrt, an exp and a log function.

Alas I have no idea where to even start on this. Can anyone provide me with some links to useful articles or, better yet, provide me with some code?

I'm assuming that once I have an exp function then it becomes relatively easy to implement pow and sqrt as they just become.

pow( x, y ) => exp( y * log( x ) )
sqrt( x )   => pow( x, 0.5 )

Its just those exp and log functions that I'm finding difficult (as though I remember a few of my log rules, I can't remember much else about them).

Presumably, there would also be a faster method for sqrt and pow so any pointers on that front would be appreciated even if its just to say use the methods i outline above.

Please note: This HAS to be cross platform and in pure C/C++ code so I cannot use any assembler optimisations.

Goz
  • 61,365
  • 24
  • 124
  • 204

4 Answers4

37

A very simple solution is to use a decent table-driven approximation. You don't actually need a lot of data if you reduce your inputs correctly. exp(a)==exp(a/2)*exp(a/2), which means you really only need to calculate exp(x) for 1 < x < 2. Over that range, a runga-kutta approximation would give reasonable results with ~16 entries IIRC.

Similarly, sqrt(a) == 2 * sqrt(a/4) == sqrt(4*a) / 2 which means you need only table entries for 1 < a < 4. Log(a) is a bit harder: log(a) == 1 + log(a/e). This is a rather slow iteration, but log(1024) is only 6.9 so you won't have many iterations.

You'd use a similar "integer-first" algorithm for pow: pow(x,y)==pow(x, floor(y)) * pow(x, frac(y)). This works because pow(double, int) is trivial (divide and conquer).

[edit] For the integral component of log(a), it may be useful to store a table 1, e, e^2, e^3, e^4, e^5, e^6, e^7 so you can reduce log(a) == n + log(a/e^n) by a simple hardcoded binary search of a in that table. The improvement from 7 to 3 steps isn't so big, but it means you only have to divide once by e^n instead of n times by e.

[edit 2] And for that last log(a/e^n) term, you can use log(a/e^n) = log((a/e^n)^8)/8 - each iteration produces 3 more bits by table lookup. That keeps your code and table size small. This is typically code for embedded systems, and they don't have large caches.

[edit 3] That's still not to smart on my side. log(a) = log(2) + log(a/2). You can just store the fixed-point value log2=0.6931471805599, count the number of leading zeroes, shift a into the range used for your lookup table, and multiply that shift (integer) by the fixed-point constant log2. Can be as low as 3 instructions.

Using e for the reduction step just gives you a "nice" log(e)=1.0 constant but that's false optimization. 0.6931471805599 is just as good a constant as 1.0; both are 32 bits constants in 10.22 fixed point. Using 2 as the constant for range reduction allows you to use a bit shift for a division.

[edit 5] And since you're storing it in Q10.22, you can better store log(65536)=11.09035488. (16 x log(2)). The "x16" means that we've got 4 more bits of precision available.

You still get the trick from edit 2, log(a/2^n) = log((a/2^n)^8)/8. Basically, this gets you a result (a + b/8 + c/64 + d/512) * 0.6931471805599 - with b,c,d in the range [0,7]. a.bcd really is an octal number. Not a surprise since we used 8 as the power. (The trick works equally well with power 2, 4 or 16.)

[edit 4] Still had an open end. pow(x, frac(y) is just pow(sqrt(x), 2 * frac(y)) and we have a decent 1/sqrt(x). That gives us the far more efficient approach. Say frac(y)=0.101 binary, i.e. 1/2 plus 1/8. Then that means x^0.101 is (x^1/2 * x^1/8). But x^1/2 is just sqrt(x) and x^1/8 is (sqrt(sqrt(sqrt(x))). Saving one more operation, Newton-Raphson NR(x) gives us 1/sqrt(x) so we calculate 1.0/(NR(x)*NR((NR(NR(x))). We only invert the end result, don't use the sqrt function directly.

MSalters
  • 173,980
  • 10
  • 155
  • 350
  • 3
    for exp and log, your approach is OK (except that I'd use Taylor or Pade expansion around 1, and use arguments between -0.5 and 0.5 for exp, and 1 and 2 for log). For sqrt, it is probably overkill: Newton method seems fairly well suited (you have to compute 1 / sqrt(x) by Newton method, only multiplications) – Alexandre C. Jan 11 '11 at 13:00
  • As an aside I've implemented sqrt as a newton raphson iteration. The performance is good and it only take a few steps to be more precise than my 10.22 fixed can cope with ... – Goz Jan 11 '11 at 21:43
  • how do you do pow(x, frac(y))? – Adam Tegen Jun 25 '13 at 18:58
  • @AdamTegen: Probably as `exp(frac(y)*log(x))`, using the optimizations above. Since `frac(y) < 1`, and `log(x)` can't be big anyway, you won't need many iterations of `exp(a)==exp(a/2)*exp(a/2)`. I might also consider `=pow(sqrt(x), 2*frac(y)`. – MSalters Jun 25 '13 at 19:25
24

Below is an example C implementation of Clay S. Turner's fixed-point log base 2 algorithm[1]. The algorithm doesn't require any kind of look-up table. This can be useful on systems where memory constraints are tight and the processor lacks an FPU, such as is the case with many microcontrollers. Log base e and log base 10 are then also supported by using the property of logarithms that, for any base n:

          logₘ(x)
logₙ(x) = ───────
          logₘ(n)

where, for this algorithm, m equals 2.

A nice feature of this implementation is that it supports variable precision: the precision can be determined at runtime, at the expense of range. The way I've implemented it, the processor (or compiler) must be capable of doing 64-bit math for holding some intermediate results. It can be easily adapted to not require 64-bit support, but the range will be reduced.

When using these functions, x is expected to be a fixed-point value scaled according to the specified precision. For instance, if precision is 16, then x should be scaled by 2^16 (65536). The result is a fixed-point value with the same scale factor as the input. A return value of INT32_MIN represents negative infinity. A return value of INT32_MAX indicates an error and errno will be set to EINVAL, indicating that the input precision was invalid.

#include <errno.h>
#include <stddef.h>

#include "log2fix.h"

#define INV_LOG2_E_Q1DOT31  UINT64_C(0x58b90bfc) // Inverse log base 2 of e
#define INV_LOG2_10_Q1DOT31 UINT64_C(0x268826a1) // Inverse log base 2 of 10

int32_t log2fix (uint32_t x, size_t precision)
{
    int32_t b = 1U << (precision - 1);
    int32_t y = 0;

    if (precision < 1 || precision > 31) {
        errno = EINVAL;
        return INT32_MAX; // indicates an error
    }

    if (x == 0) {
        return INT32_MIN; // represents negative infinity
    }

    while (x < 1U << precision) {
        x <<= 1;
        y -= 1U << precision;
    }

    while (x >= 2U << precision) {
        x >>= 1;
        y += 1U << precision;
    }

    uint64_t z = x;

    for (size_t i = 0; i < precision; i++) {
        z = z * z >> precision;
        if (z >= 2U << (uint64_t)precision) {
            z >>= 1;
            y += b;
        }
        b >>= 1;
    }

    return y;
}

int32_t logfix (uint32_t x, size_t precision)
{
    uint64_t t;

    t = log2fix(x, precision) * INV_LOG2_E_Q1DOT31;

    return t >> 31;
}

int32_t log10fix (uint32_t x, size_t precision)
{
    uint64_t t;

    t = log2fix(x, precision) * INV_LOG2_10_Q1DOT31;

    return t >> 31;
}

The code for this implementation also lives at Github, along with a sample/test program that illustrates how to use this function to compute and display logarithms from numbers read from standard input.

[1] C. S. Turner, "A Fast Binary Logarithm Algorithm", IEEE Signal Processing Mag., pp. 124,140, Sep. 2010.

Dan Moulding
  • 211,373
  • 23
  • 97
  • 98
  • What exactly do you mean by "precision"? Is that the number of bits used for the fractional part? I.e. precision=10 would mean, that a int32_t variable is interpreted as a floating point number with 1 sign bit, 21 bits integer part and 10 bits fractional part. Is that correct? – Joerg Dec 17 '15 at 10:31
  • @Joerg Yes, except there is no sign bit (the input value, x, is unsigned since the real logarithm is undefined for negative values). So for precision 10, there are 22 integer bits and 10 fractional bits. – Dan Moulding Dec 17 '15 at 18:35
  • @DanMoulding is it possible to use this technique to calculate a power of two with fixed points? I asked another question to that regard: https://stackoverflow.com/questions/61471447/how-to-reverse-fixed-point-binary-log-algorithm-for-power-of-2 – Sophia Gold Apr 28 '20 at 02:05
  • Thanks for the reference. This is a really pretty algorithm and trivial to port due to its simplicity. – wchargin Apr 23 '21 at 18:07
5

A good starting point is Jack Crenshaw's book, "Math Toolkit for Real-Time Programming". It has a good discussion of algorithms and implementations for various transcendental functions.

Paul R
  • 208,748
  • 37
  • 389
  • 560
4

Check my fixed point sqrt implementation using only integer operations. It was fun to invent. Quite old now.

https://groups.google.com/forum/?hl=fr%05aacf5997b615c37&fromgroups#!topic/comp.lang.c/IpwKbw0MAxw/discussion

Otherwise check the CORDIC set of algorithms. That's the way to implement all the functions you listed and the trigonometric functions.

EDIT : I published the reviewed source on GitHub here

chmike
  • 20,922
  • 21
  • 83
  • 106