Search code examples
cmathembeddedlogarithmlookup-tables

Compute logarithmic expression without floating point arithmetics or log


I need to compute the mathematical expression floor(ln(u)/ln(1-p)) for 0 < u < 1 and 0 < p < 1 in C on an embedded processor with no floating point arithmetics and no ln function. The result is a positive integer. I know about the limit cases (p=0), I'll deal with them later...

I imagine that the solution involves having u and p range over 0..UINT16_MAX, and appeal to a lookup table for the logarithm, but I cannot figure out how exactly: what does the lookup table map to?

The result needs not be 100% exact, approximations are OK.

Thanks!


Solution

  • Since the logarithm is used in both dividend and divisor, there is no need to use log(); we can use log2() instead. Due to the restrictions on the inputs u and p the logarithms are known to be both negative, so we can restrict ourselves to compute the positive quantity -log2().

    We can use fixed-point arithmetic to compute the logarithm. We do so by multiplying the original input by a sequence of factors of decreasing magnitude that approach 1. Considering each of the factor in sequence, we multiply the input only by those factors that result in a product closer to 1, but without exceeding it. While doing so, we sum the log2() of the factors that "fit". At the end of this procedure we wind up with a number very close to 1 as our final product, and a sum that represents the binary logarithm.

    This process is known in the literature as multiplicative normalization or pseudo division, and some early publications describing it are the works by De Lugish and Meggitt. The latter indicates that the origin is basically Henry Briggs's method for computing common logarithms.

    B. de Lugish. "A Class of Algorithms for Automatic Evaluation of Functions and Computations in a Digital Computer". PhD thesis, Dept. of Computer Science, University of Illinois, Urbana, 1970.

    J. E. Meggitt. "Pseudo division and pseudo multiplication processes". IBM Journal of Research and Development, Vol. 6, No. 2, April 1962, pp. 210-226

    As the chosen set of factors comprises 2i and (1+2-i) the necessary multiplications can be performed without the need for a multiplication instruction: the products can be computed by either shift or shift plus add.

    Since the inputs u and p are purely fractional numbers with 16 bits, we may want to chose a 5.16 fixed-point result for the logarithm. By simply dividing the two logarithm values, we remove the fixed-point scale factor, and apply a floor() operation at the same time, because for positive numbers, floor(x) is identical to trunc(x) and integer division is truncating.

    Note that the fixed-point computation of the logarithm results in large relative error for inputs near 1. This in turn means the entire function computed using fixed-point arithmetic may deliver results significantly different from the reference if p is small. An example of this is the following test case: u=55af p=0052 res=848 ref=874.

    #include <stdlib.h>
    #include <stdio.h>
    #include <stdint.h>
    
    /* input x is a 0.16 fixed-point number in [0,1)
       function returns -log2(x) as a 5.16 fixed-point number in (0, 16]
    */   
    uint32_t nlog2_16 (uint16_t x)
    {
        uint32_t r = 0;
        uint32_t t, a = x;
    
        /* try factors 2**i with i = 8, 4, 2, 1 */
        if ((t = a << 8       ) < 0x10000) { a = t; r += 0x80000; }
        if ((t = a << 4       ) < 0x10000) { a = t; r += 0x40000; }
        if ((t = a << 2       ) < 0x10000) { a = t; r += 0x20000; }
        if ((t = a << 1       ) < 0x10000) { a = t; r += 0x10000; }
        /* try factors (1+2**(-i)) with i = 1, .., 16 */
        if ((t = a + (a >>  1)) < 0x10000) { a = t; r += 0x095c0; }
        if ((t = a + (a >>  2)) < 0x10000) { a = t; r += 0x0526a; }
        if ((t = a + (a >>  3)) < 0x10000) { a = t; r += 0x02b80; }
        if ((t = a + (a >>  4)) < 0x10000) { a = t; r += 0x01664; }
        if ((t = a + (a >>  5)) < 0x10000) { a = t; r += 0x00b5d; }
        if ((t = a + (a >>  6)) < 0x10000) { a = t; r += 0x005ba; }
        if ((t = a + (a >>  7)) < 0x10000) { a = t; r += 0x002e0; }
        if ((t = a + (a >>  8)) < 0x10000) { a = t; r += 0x00171; }
        if ((t = a + (a >>  9)) < 0x10000) { a = t; r += 0x000b8; }
        if ((t = a + (a >> 10)) < 0x10000) { a = t; r += 0x0005c; }
        if ((t = a + (a >> 11)) < 0x10000) { a = t; r += 0x0002e; }
        if ((t = a + (a >> 12)) < 0x10000) { a = t; r += 0x00017; }
        if ((t = a + (a >> 13)) < 0x10000) { a = t; r += 0x0000c; }
        if ((t = a + (a >> 14)) < 0x10000) { a = t; r += 0x00006; }
        if ((t = a + (a >> 15)) < 0x10000) { a = t; r += 0x00003; }
        if ((t = a + (a >> 16)) < 0x10000) { a = t; r += 0x00001; }
        return r;
    }
    
    /* Compute floor(log(u)/log(1-p)) for 0 < u < 1 and 0 < p < 1,
       where 'u' and 'p' are represented as 0.16 fixed-point numbers
       Result is an integer in range [0, 1048676]
    */
    uint32_t func (uint16_t u, uint16_t p)
    {
        uint16_t one_minus_p = 0x10000 - p; // 1.0 - p
        uint32_t log_u = nlog2_16 (u);
        uint32_t log_p = nlog2_16 (one_minus_p);
        uint32_t res = log_u / log_p;  // divide and floor in one go
        return res;
    }