we have sin(x) and exp2(x) now. only log2(x) is missing. well, our sin(x) is stolen from wikipedia for now. maybe we'll eventually learn how to make our own.
just like with exp2(x), there are binary-level hacks that gets us pretty far. maybe it makes more than a little sense because
x = log2(exp2(x)) x = exp2(log2(x))so either is the "inverse" of the other. let's have a look
last time we got decent results for exp2(x), almost for free, by lerping x into large integers and re-interpreting them as float32 values. this time we're doing it the other way around, by converting the number directly to binary, and then returning the value, almost as-is, after "unlerping" it.
we can also do a stairstep version (the stairstep version was easiest last time?)
if you have trouble understanding what's going on with the bit-shifts, binary-level stuff and such, we went into more detail in the previous article.
in any case, look at the stairstep version and convince yourself that the "missing correction" this time is not a multiplier, but an "adder". that's because the steps are all 1 unit tall today. with exp2(x), each step was twice as tall as the previous one. so last time our solution was:
our_exp2(x) = stairstep_exp2(x) * taylorseries_exp2(x-floor(x))but this time it might be more like
our_log2(x) = stairstep_log2(x) + taylorseries_log2(...?)i'm writing "...?" as argument for taylorseries_log2() because while each exp2(x) step was 1 wide, this time each log2(x) step is twice as wide as the previous one. so you could almost say that log2(x) is exp2(x) rotated 90Β° (which isn't as silly as it might sound).
however, we almost have the answer already for how wide a step is: look at the step that's 4 wide. its y-value is 2. so the width is 1<<2=4. so let's cheat and make a "corrective adder" from log2(x) itself, just so we get a feel for what we have to approximate next.
so we also need an exp2(x), huh (for 2**Β±stairstep). but the input is always an integer, so we can copy it into the exponent of a float32 and apply bias (so exp2(floor(x)) = u32_to_f32((x+127)<<23)). (read ahead for a much simpler solution to calculate xc. hint: it also involves the other bit-hackery function, u32_to_f32())
so lets do some taylor series for log2(x), for x between 1 and 2. meaning we need to be able to differentiate log2(x) as we learned earlier. let's use maxima again (i like that you can just paste the repl-session into a pre-tag)
on the train without internet i couldn't figure out if maxima has a specific function for log2 (or logn: log in any base), but instead we can just use the math equivalence that log2(x)=log(x)/log(2) (with log(x) being the "natural logarithm", or the base-e logarithm):
(%i1) diff(log(x)/log(2),x,0);
log(x)
(%o1) ββββββ
log(2)
(%i2) diff(log(x)/log(2),x,1);
1
(%o2) ββββββββ
log(2) x
(%i3) diff(log(x)/log(2),x,2);
1
(%o3) - βββββββββ
2
log(2) x
(%i4) diff(log(x)/log(2),x,3);
2
(%o4) βββββββββ
3
log(2) x
(%i5) diff(log(x)/log(2),x,4);
6
(%o5) - βββββββββ
4
log(2) x
the pattern/sequence of the fraction numerator may seem a little weird
(1,-1,2,-6,...): to find the next numerator, multiply the current
numerator with the current order, and flip the sign. so the next
numerator is 24:
note: maxima doesn't give you an error for log2(x). it just assumes it's a function of some kind and gives you the differentiation syntax (in Leibniz notation), like saying "yeah the 3rd derivative of log2(x) is the 3rd derivative of log2(x)":
(%i9) diff(log2(x),x,3);
3
d
(%o9) βββ (log2(x))
3
dx
less talk more taylor
not bad! i get the best results with auto-calibrate on, but it's interesting that it usually has a sweet spot that's not quite a=1.5. for order=3 it's around a=1.473, improving maxerr=0.0016 to maxerr=0.0013. it's also wild how much worse the taylor series performs for a=1 where it hardly converges at all (try turning auto-calibrate off to see how bad it is), and even a=2 is much better. before i started plotting and making tools i couldn't see these things, and just kinda assumed taylor series were terrible for log2(x), and maybe they are a little bit, but not as much as i thought.
the one dough.c "steals" at the time of writing uses division, and taylor series don't (being polynomials). so apparently there are other ways of approximating functions :) i also tried correcting the smoother non-stairstep one from the top, but the error is the same, so i prefer the stairstep one (slightly simpler code)
let's have a look at the "stolen one" using division.
roughly as good as a 6th order above, but for a lot less work. also, the line
const f0 = u32_to_f32((u0 & ((1<<23)-1)) | (126<<23));made me realise i could replace
const stairstep = (f32_to_u32(x) >> 23) - 127; const stairexp = u32_to_f32((stairstep+127)<<23); const invstairexp = u32_to_f32((-stairstep+127)<<23); const xc = 1+(x-stairexp)*invstairexp;with
const xc = u32_to_f32((f32_to_u32(x) & ((1<<23)-1)) | (127<<23));it replaces the exponent of the float32 with 127, squeezing any ..,[1/2;1], [1;2], [2;4], [4;8], [8,16],.. range into [1;2]. the previous code did that by trying to "unlerp" x from its stairstep width lol.