19

I'm looking for implementation of log() and exp() functions provided in C library <math.h>. I'm working with 8 bit microcontrollers (OKI 411 and 431). I need to calculate Mean Kinetic Temperature. The requirement is that we should be able to calculate MKT as fast as possible and with as little code memory as possible. The compiler comes with log() and exp() functions in <math.h>. But calling either function and linking with the library causes the code size to increase by 5 Kilobytes, which will not fit in one of the micro we work with (OKI 411), because our code already consumed ~12K of available ~15K code memory.

The implementation I'm looking for should not use any other C library functions (like pow(), sqrt() etc). This is because all library functions are packed in one library and even if one function is called, the linker will bring whole 5K library to code memory.

EDIT

The algorithm should be correct up to 3 decimal places.

duffymo
  • 305,152
  • 44
  • 369
  • 561
Donotalo
  • 12,748
  • 25
  • 83
  • 121
  • 3
    When you have such limitations, you should be also asking yourself what is the precision you can accept ? So what's the acceptable error margin ? – Yochai Timmer Mar 21 '12 at 05:29
  • 1
    @YochaiTimmer: forgot to add. thanks for reminding. i've edited my question. :) – Donotalo Mar 21 '12 at 05:37
  • Also, what are the input and output numeric formats? Fixed-point such as 8.8? It sounds like you would benefit by storing an offset relative to 273 kelvins, i.e. Celsius. – Potatoswatter Mar 21 '12 at 05:38
  • @Potatoswatter: the input/output is not any concern. what do you mean by 'bias relative to 273K'? – Donotalo Mar 21 '12 at 05:40
  • @Donotalo Because 273 is a large number relative to the value of the temperature in Celsius, you can get more precision from the same bits by storing Celsius instead of Kelvin. Actually this illustrates why the input/output *is* a concern. As Alexei mentions, the temperature range affects the choice of formula. – Potatoswatter Mar 21 '12 at 05:43
  • Possible duplicate of [Creating logarithm function in C](https://stackoverflow.com/questions/5862699/creating-logarithm-function-in-c) – 7vujy0f0hy Jan 01 '19 at 02:30

8 Answers8

21

Using Taylor series is not the simplest neither the fastest way of doing this. Most professional implementations are using approximating polynomials. I'll show you how to generate one in Maple (it is a computer algebra program), using the Remez algorithm.

For 3 digits of accuracy execute the following commands in Maple:

with(numapprox):
Digits := 8
minimax(ln(x), x = 1 .. 2, 4, 1, 'maxerror')
maxerror

Its response is the following polynomial:

-1.7417939 + (2.8212026 + (-1.4699568 + (0.44717955 - 0.056570851 * x) * x) * x) * x

With the maximal error of: 0.000061011436

Remez approximation of a function

We generated a polynomial which approximates the ln(x), but only inside the [1..2] interval. Increasing the interval is not wise, because that would increase the maximal error even more. Instead of that, do the following decomposition:

formula

So first find the highest power of 2, which is still smaller than the number (See: What is the fastest/most efficient way to find the highest set bit (msb) in an integer in C?). That number is actually the base-2 logarithm. Divide with that value, then the result gets into the 1..2 interval. At the end we will have to add n*ln(2) to get the final result.

An example implementation for numbers >= 1:

float ln(float y) {
    int log2;
    float divisor, x, result;

    log2 = msb((int)y); // See: https://stackoverflow.com/a/4970859/6630230
    divisor = (float)(1 << log2);
    x = y / divisor;    // normalized value between [1.0, 2.0]

    result = -1.7417939 + (2.8212026 + (-1.4699568 + (0.44717955 - 0.056570851 * x) * x) * x) * x;
    result += ((float)log2) * 0.69314718; // ln(2) = 0.69314718

    return result;
}

Although if you plan to use it only in the [1.0, 2.0] interval, then the function is like:

float ln(float x) {
    return -1.7417939 + (2.8212026 + (-1.4699568 + (0.44717955 - 0.056570851 * x) * x) * x) * x;
}
Crouching Kitten
  • 1,135
  • 12
  • 23
  • What would be the implementation for numbers between ]0,1] ? – Thadeu Melo May 08 '18 at 13:59
  • @ThadeuAntonioFerreiraMelo : I would estimate ]0; 1] with another polynomial, and use an if/else statement to decide which to use. This is that polynomyal (for this low precision): `-8.6731532+(129.946172+(-558.971892+(843.967330-409.109529*x)*x)*x)*x` – Crouching Kitten May 12 '18 at 08:26
  • Actually you can find why this isn't selected as right answer. + with some search one can find ready to go implementation in mathematics sun library implementation (msun). – pholat Sep 26 '18 at 14:15
  • 1
    Apologies; I downvoted your answer because the code didn't work on a C++ playground I tried it on. I have since tried it in Visual Studio, where it worked; but I cannot change my vote; it says "your vote is locked in." I do not know how to fix this. – Narf the Mouse Oct 31 '20 at 01:40
9

The Taylor series for e^x converges extremely quickly, and you can tune your implementation to the precision that you need. (http://en.wikipedia.org/wiki/Taylor_series)

The Taylor series for log is not as nice...

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
dsharlet
  • 1,036
  • 1
  • 8
  • 15
  • if my calculations are correct, the input for `ln` will be in range [0.94, 0.98]. i guess taylor series is good enough for approximation for `ln` too. – Donotalo Mar 21 '12 at 06:49
  • 1
    @Donotalo I'm late to the party here. Just be aware that the range you specified is in the "not nice" part of the Taylor series for the natural logarithm: https://en.wikipedia.org/wiki/Natural_logarithm#Series – Spencer Jun 21 '18 at 12:57
  • @Eddy: The Taylor series approach is not efficient since the convergence is slow. Instead, use Newton's method, which has cubic convergence to ln(x). [See my answer](https://stackoverflow.com/a/63773160/155077) – Stefan Steiger Sep 07 '20 at 07:37
7

If you don't need floating-point math for anything else, you may compute an approximate fractional base-2 log pretty easily. Start by shifting your value left until it's 32768 or higher and store the number of times you did that in count. Then, repeat some number of times (depending upon your desired scale factor):

n = (mult(n,n) + 32768u) >> 16; // If a function is available for 16x16->32 multiply
count<<=1;
if (n < 32768) n*=2; else count+=1;

If the above loop is repeated 8 times, then the log base 2 of the number will be count/256. If ten times, count/1024. If eleven, count/2048. Effectively, this function works by computing the integer power-of-two logarithm of n**(2^reps), but with intermediate values scaled to avoid overflow.

supercat
  • 77,689
  • 9
  • 166
  • 211
  • There is a small bug in the above algorithm: After shifting the value, count has to be replaced with 15 - count. Aside from that, is there a more detailed explanation of the algorithm? – user3528637 Nov 30 '17 at 11:59
  • Does this algorithm have a name? – Peter Mortensen Oct 01 '18 at 18:37
  • @PeterMortensen: I don't know of a name for it. I devised it when I needed to measure the RC time constant of a circuit given two ADC readings taken a known time apart (compute the log of each reading, and take take the scaled reciprocal of the difference), but I'd be shocked if I was the first to use that approach. To see what the algorithm is doing, it may be helpful to consider what would happen if it used arbitrary-precision integers and instead of shifting n right by 16 bits each time, it shifted the 32768 left by 16 bits. – supercat Oct 01 '18 at 18:52
  • The goal of the function is then to find the value of `count` such that if `n0` is the starting value of `n`, `n0**rounds * 2**count` would be in the range `2**(16*rounds-1) and `2**(16*rounds)-1`. – supercat Oct 01 '18 at 19:02
  • I didn't do any formal analysis of the worst-case error bounds for this general approach, but since the input was a 16-bit value I simply wrote a program to test all 65535 input values and confirmed that the result was within tolerance (+/-1 IIRC) for all of them. – supercat Oct 01 '18 at 19:04
  • 1
    In case you're not aware of the reason for the renewed interest in this answer: https://space.stackexchange.com/questions/30952/how-did-the-apollo-computers-evaluate-transcendental-functions-like-sine-arctan/31050#31050 – m69's been on strike for years Oct 02 '18 at 15:40
  • @m69: I'd seen the sine/cos there, but not the log. I suppose a simplified description of my algorithm is that it tries to estimate how many shifts would be required to normalize n**(2**reps) without having to compute the intermediate products to full precision. By the description, it sounds like the AGC used a similar approach. – supercat Oct 02 '18 at 16:01
6

Necromancing.
I had to implement logarithms on rational numbers.

This is how I did it:

Occording to Wikipedia, there is the Halley-Newton approximation method

Halley-Newton Method

which can be used for very-high precision.

Using Newton's method, the iteration simplifies to (implementation), which has cubic convergence to ln(x), which is way better than what the Taylor-Series offers.

// Using Newton's method, the iteration simplifies to (implementation) 
// which has cubic convergence to ln(x).
public static double ln(double x, double epsilon)
{
    double yn = x - 1.0d; // using the first term of the taylor series as initial-value
    double yn1 = yn;

    do
    {
        yn = yn1;
        yn1 = yn + 2 * (x - System.Math.Exp(yn)) / (x + System.Math.Exp(yn));
    } while (System.Math.Abs(yn - yn1) > epsilon);

    return yn1;
}

This is not C, but C#, but I'm sure anybody capable to program in C will be able to deduce the C-Code from that.

Furthermore, since

logn(x) = ln(x)/ln(n).

You have therefore just implemented logN as well.

public static double log(double x, double n, double epsilon)
{
    return ln(x, epsilon) / ln(n, epsilon);
}

where epsilon (error) is the minimum precision.

Now as to speed, you're probably better of using the ln-cast-in-hardware, but as I said, I used this as a base to implement logarithms on a rational numbers class working with arbitrary precision.

Arbitrary precision might be more important than speed, under certain circumstances.

Then, use the logarithmic identities for rational numbers:
logB(x/y) = logB(x) - logB(y)

Here you go for the C-version:

// compile with: gcc loga.c -lm -o loga

#include <stdio.h>
#include <math.h>

double ln(double x, double epsilon)
{
    double yn = x - 1.0; // using the first term of the Taylor series as initial-value
    double yn1 = yn;

    do
    {
        yn = yn1;
        yn1 = yn + 2 * (x - exp(yn)) / (x + exp(yn));
    } while (fabs(yn - yn1) > epsilon);

    return yn1;
}

int main()
{
    double x = 2.0; // You can change the values as needed
    double epsilon = 1e-6;
    double result = ln(x, epsilon);

    printf("ln(%lf) = %lf\n", x, result);

    return 0;
}
Stefan Steiger
  • 78,642
  • 66
  • 377
  • 442
  • The problem with this code is its using another function `exp` – Onyambu Mar 07 '23 at 05:18
  • You can always implement that. double Exp (double d); - The number e raised to the power d. Find an approximation for e that matches your accuracy-requirements, and off you go. – Stefan Steiger Mar 07 '23 at 14:49
6

Would basic table with interpolation between values approach work? If ranges of values are limited (which is likely for your case - I doubt temperature readings have huge range) and high precisions is not required it may work. Should be easy to test on normal machine.

Here is one of many topics on table representation of functions: Calculating vs. lookup tables for sine value performance?

Community
  • 1
  • 1
Alexei Levenkov
  • 98,904
  • 14
  • 127
  • 179
  • the range of temperature is -22F to 158F (-30C to 70C) with 0.1F increment. there is 1800 possible temperature points and i guess a lookup table isn't enough. – Donotalo Mar 21 '12 at 06:51
  • @Donotalo, It still may be an option to try (also may not work for your needed precision) - both exp/ln functions are continuous, so you may need much less points for required precision of the result. I don't see temperature directly used as argument of exp/ln in the formula, so actual ranges for arguments are different - it is hard to predict if sparse table would work. – Alexei Levenkov Mar 21 '12 at 16:12
1

In addition to Crouching Kitten's answer which gave me inspiration, you can build a pseudo-recursive (at most 1 self-call) logarithm to avoid using polynomials. In pseudo code

ln(x) :=
If (x <= 0)
    return NaN
Else if (!(1 <= x < 2))
    return LN2 * b + ln(a)
Else
    return taylor_expansion(x - 1)

This is pretty efficient and precise since on [1; 2) the taylor series converges A LOT faster, and we get such a number 1 <= a < 2 with the first call to ln if our input is positive but not in this range.

You can find 'b' as your unbiased exponent from the data held in the float x, and 'a' from the mantissa of the float x (a is exactly the same float as x, but now with exponent biased_0 rather than exponent biased_b). LN2 should be kept as a macro in hexadecimal floating point notation IMO. You can also use http://man7.org/linux/man-pages/man3/frexp.3.html for this.

Also, the trick

unsigned long tmp = *(ulong*)(&d);

for "memory-casting" double to unsigned long, rather than "value-casting", is very useful to know when dealing with floats memory-wise, as bitwise operators will cause warnings or errors depending on the compiler.

Tristan Duquesne
  • 512
  • 1
  • 6
  • 16
  • Nice idea to make use of the internal float representation. What is the reason for the `- 1` at the Taylor expansion? The problem I see is that the mantissa is an integer, and not in the `[1; 2)` range, and the `frexp` actually makes some computation to normalize the value. – Crouching Kitten Apr 05 '18 at 11:53
  • This is a simplified version of my code, I have some optimizations handy. - the y = x - 1 <=> ln(1 + y) = ln(x) is used to call taylor_expansion as ln(1 + y) = ln(x) = taylor(y) = y - y^2/2 + y^3/3... It's just an ease of implementation, but it's true it might be a little confusing at first. – Tristan Duquesne Apr 06 '18 at 14:19
  • - I also have an **else if (1.9 <= x < 2) return ln(x * 2 / 3) + ln (3/2);** clause to prevent numbers extremely close to their ceiling from making the function time out (try the algorithm with a value like 1e+23 (which is closer to 9.9999999999999e+22), you'll see without this small fix some cases are apocalyptically slow. Note that I keep 2/3 and ln(3/2) as macro hexadecimal floating point constants.) Gotta get that log to converge ! x) – Tristan Duquesne Apr 06 '18 at 14:20
  • - as for frexp, I go homemade: => `u64 extract = *(u64*)(&d)`; => `norm = (extract & (0x8000000000000000 | 0xFFFFFFFFFFFFF)) | 0x3FF0000000000000;` and `d = *(double*)(&norm);` for my normalized fraction (exp == 0) => `exp = ((extract << 1) >> 53) - 1023` for my exponent. You can check for other special values as well when starting your log (ie, +inf returns +inf, 1. returns 0., etc). Note that => serve to signify a \n in my comment plaintext here xd – Tristan Duquesne Apr 06 '18 at 14:35
1

Possible computation of ln(x) and expo(x) in C without <math.h> :

static double expo(double n) {
    int a = 0, b = n > 0;
    double c = 1, d = 1, e = 1;
    for (b || (n = -n); e + .00001 < (e += (d *= n) / (c *= ++a)););
    // approximately 15 iterations
    return b ? e : 1 / e;
}

static double native_log_computation(const double n) {
    // Basic logarithm computation.
    static const double euler = 2.7182818284590452354 ;
    unsigned a = 0, d;
    double b, c, e, f;
    if (n > 0) {
        for (c = n < 1 ? 1 / n : n; (c /= euler) > 1; ++a);
        c = 1 / (c * euler - 1), c = c + c + 1, f = c * c, b = 0;
        for (d = 1, c /= 2; e = b, b += 1 / (d * c), b - e/* > 0.0000001 */;)
            d += 2, c *= f;
    } else b = (n == 0) / 0.;
    return n < 1 ? -(a + b) : a + b;
}

static inline double native_ln(const double n) {
    //  Returns the natural logarithm (base e) of N.
    return native_log_computation(n) ;
}

static inline double native_log_base(const double n, const double base) {
    //  Returns the logarithm (base b) of N.
    return native_log_computation(n) / native_log_computation(base) ;
}

Try it Online

Michel
  • 259
  • 2
  • 3
0

Building off @Crouching Kitten's great natural log answer above, if you need it to be accurate for inputs <1 you can add a simple scaling factor. Below is an example in C++ that i've used in microcontrollers. It has a scaling factor of 256 and it's accurate to inputs down to 1/256 = ~0.04, and up to 2^32/256 = 16777215 (due to overflow of a uint32 variable).

It's interesting to note that even on an STMF103 Arm M3 with no FPU, the float implementation below is significantly faster (eg 3x or better) than the 16 bit fixed-point implementation in libfixmath (that being said, this float implementation still takes a few thousand cycles so it's still not ~fast~)

#include <float.h>

float TempSensor::Ln(float y) 
{
    // Algo from: https://stackoverflow.com/a/18454010
    // Accurate between (1 / scaling factor) < y < (2^32  / scaling factor). Read comments below for more info on how to extend this range

    float divisor, x, result;
    const float LN_2 = 0.69314718; //pre calculated constant used in calculations
    uint32_t log2 = 0;


    //handle if input is less than zero
    if (y <= 0)
    {
        return -FLT_MAX;
    }

    //scaling factor. The polynomial below is accurate when the input y>1, therefore using a scaling factor of 256 (aka 2^8) extends this to 1/256 or ~0.04. Given use of uint32_t, the input y must stay below 2^24 or 16777216 (aka 2^(32-8)), otherwise uint_y used below will overflow. Increasing the scaing factor will reduce the lower accuracy bound and also reduce the upper overflow bound. If you need the range to be wider, consider changing uint_y to a uint64_t
    const uint32_t SCALING_FACTOR = 256;
    const float LN_SCALING_FACTOR = 5.545177444; //this is the natural log of the scaling factor and needs to be precalculated

    y = y * SCALING_FACTOR; 
    
    uint32_t uint_y = (uint32_t)y;
    while (uint_y >>= 1) // Convert the number to an integer and then find the location of the MSB. This is the integer portion of Log2(y). See: https://stackoverflow.com/a/4970859/6630230
    {
        log2++;
    }

    divisor = (float)(1 << log2);
    x = y / divisor;    // FInd the remainder value between [1.0, 2.0] then calculate the natural log of this remainder using a polynomial approximation
    result = -1.7417939 + (2.8212026 + (-1.4699568 + (0.44717955 - 0.056570851 * x) * x) * x) * x; //This polynomial approximates ln(x) between [1,2]

    result = result + ((float)log2) * LN_2 - LN_SCALING_FACTOR; // Using the log product rule Log(A) + Log(B) = Log(AB) and the log base change rule log_x(A) = log_y(A)/Log_y(x), calculate all the components in base e and then sum them: = Ln(x_remainder) + (log_2(x_integer) * ln(2)) - ln(SCALING_FACTOR)

    return result;
}
Fergus
  • 1
  • 1