I previously showed and explained how to compute the binary logarithm of a pure fraction (that is, 0 < a < 1) in fixed-point arithmetic. As the asker notes, the integer portion of a binary logarithm can be computed using a count-leading-zero primitive (clz
). One therefore splits the integer input x
like so: x = a * 2**expo
, and computes expo
via clz
and log2(a)
using the algorithm I demonstrated previously.
I do not know Rust, so the code below was written in ISO-C99, which I hope can be translated to Rust in a straightforward manner. The clz
functionality is implemented manually here. Obviously one would want to avail oneself of appropriate built-ins for this where available.
The specification given by asker stated that the result be returned in UQ64.64 format which seemed unusual to me, so the ilog2()
function here returns a UQ16.16 result. If a result in UQ64.64 format is definitely required, a conversion is easily accomplished by converting the result of ilog2()
to a __uint128_t
and performing a left shift by 48 on he result.
I have included some light-weight test scaffolding to demonstrate the correct operation of the code. Using built-ins for clz
should result in about 50 to 60 machine instructions being generated on common CPU architectures like x86-64 and ARM64 when using the latest compilers, so performance should be sufficient for various use cases (the asker did not mention specific performance goals).
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <inttypes.h>
#include <math.h>
#define TEST_LIMIT (1ULL << 25)
uint32_t clz32 (uint32_t x)
{
uint32_t n = 32;
uint32_t y;
y = x >> 16; if (y != 0) { n = n - 16; x = y; }
y = x >> 8; if (y != 0) { n = n - 8; x = y; }
y = x >> 4; if (y != 0) { n = n - 4; x = y; }
y = x >> 2; if (y != 0) { n = n - 2; x = y; }
y = x >> 1; if (y != 0) return n - 2;
return n - x;
}
uint32_t clz64 (uint64_t a)
{
uint32_t ahi = (uint32_t)(a >> 32);
uint32_t alo = (uint32_t)(a);
uint32_t res;
if (ahi) {
res = 0;
} else {
res = 32;
ahi = alo;
}
res = res + clz32 (ahi);
return res;
}
/* compute log2(x) for x in [1,2**64-1]; result is a UQ16.16 accurate to 6 fractional bits */
uint32_t ilog2 (uint64_t x)
{
uint32_t a, t, one = ((uint32_t)1) << 31; // UQ1.31
uint32_t r = 0; // UQ16.16
uint32_t lz, expo;
/* split: x = a * 2**expo, 0 < a < 1 */
lz = clz64 (x);
expo = 64 - lz;
a = (uint32_t)((x << lz) >> (64 - 31)); // UQ1.31
/* try factors (1+2**(-i)) with i = 1, ..., 8 */
if ((t = a + (a >> 1)) < one) { a = t; r += 0x095c0; }
if ((t = a + (a >> 2)) < one) { a = t; r += 0x0526a; }
if ((t = a + (a >> 3)) < one) { a = t; r += 0x02b80; }
if ((t = a + (a >> 4)) < one) { a = t; r += 0x01664; }
if ((t = a + (a >> 5)) < one) { a = t; r += 0x00b5d; }
if ((t = a + (a >> 6)) < one) { a = t; r += 0x005ba; }
if ((t = a + (a >> 7)) < one) { a = t; r += 0x002e0; }
if ((t = a + (a >> 8)) < one) { a = t; r += 0x00171; }
r = (expo << 16) - r; // adjust for split of argument
r = ((r + (1 << 9)) >> (16 - 6)) << (16 - 6); // round to 6 fractional bits
return r;
}
int main (void)
{
uint32_t ir;
uint64_t errloc = 0, ix = 1;
const double two_to_16 = 65536.0;
double res, ref;
double err, maxerr = 0;
do {
ir = ilog2 (ix);
res = (double)ir / two_to_16;
ref = log2 ((double)ix);
err = fabs (res - ref);
if (err > maxerr) {
maxerr = err;
errloc = ix;
}
ix += 1;
if ((ix & 0xffffff) == 0) printf ("\r%" PRIx64, ix);
} while (ix < TEST_LIMIT);
printf ("\nmaxerr = %.8f @ %" PRIu64 "\n", maxerr, errloc);
return EXIT_SUCCESS;
}