Calculating Floating Point Powers (PHP/BCMath)

algorithm, bcmath, math, php, pow

Solution

The employed algorithm to find the nth root of a (positive) number `a` is the Newton algorithm for finding the zero of

f(x) = x^n - a.

That involves only powers with natural numbers as exponents, hence is straightforward to implement.

Calculating a power with an exponent `0 < y < 1` where `y` is not of the form `1/n` with an integer `n` is more complicated. Doing the analogue, solving

x^(1/y) - a == 0

would again involve calculating a power with non-integral exponent, the very problem we're trying to solve.

If `y = n/d` is rational with small denominator `d`, the problem is easily solved by calculating

x^(n/d) = (x^n)^(1/d),

but for most rational `0 < y < 1`, numerator and denominator are rather large, and the intermediate `x^n` would be huge, so the computation would use a lot of memory and take a (relatively) long time. (For the example exponent of `0.123456 = 1929/15625`, it's not too bad, but `0.1234567` would be rather taxing.)

One way to calculate the power for general rational `0 < y < 1` is to write

y = 1/a ± 1/b ± 1/c ± ... ± 1/q

with integers `a < b < c < ... < q` and to multiply/divide the individual `x^(1/k)`. (Every rational `0 < y < 1` has such representations, and the shortest such representations generally don't involve many terms, e.g.

1929/15625 = 1/8 - 1/648 - 1/1265625;

using only additions in the decomposition leads to longer representations with larger denominators, e.g.

1929/15625 = 1/9 + 1/82 + 1/6678 + 1/46501020 + 1/2210396922562500,

so that would involve more work.)

Some improvement is possible by mixing the approaches, first find a close rational approximation to `y` with small denominator via the continued fraction expansion of `y` - for the example exponent `1929/15625 = [0;8,9,1,192]` and using the first four partial quotients yields the approximation `10/81 = 0.123456790123...` [note that `10/81 = 1/8 - 1/648`, the partial sums of the shortest decomposition into pure fractions are convergents] - and then decompose the remainder into pure fractions.

However, in general that approach leads to calculating nth roots for large `n`, which also is slow and memory-intensive if the desired accuracy of the final result is high.

All in all, it is probably simpler and faster to implement `exp` and `log` and use

x^y = exp(y*log(x))

Problem

I'm writing a wrapper for the `bcmath` extension, and bug #10116 regarding `bcpow()` is particularly annoying -- it casts the `$right_operand` (`$exp`) to an (native PHP, not arbitrary length) integer, so when you try to calculate the square root (or any other root higher than `1`) of a number you always end up with `1` instead of the correct result. I started searching for algorithms that would allow me to calculate the nth root of a number and I found this answer which looks pretty solid, I actually expanded the formula using WolframAlpha and I was able to improve it's speed by about 5% while keeping the accuracy of the results. Here is a pure PHP implementation mimicking my BCMath implementation and its limitations: ``` function _pow($n, $exp) { $result = pow($n, intval($exp)); // bcmath casts $exp to (int) if (fmod($exp, 1) > 0) // does $exp have a fracional part higher than 0? { $exp = 1 / fmod($exp, 1); // convert the modulo into a root (2.5 -> 1 / 0.5 = 2) $x = 1; $y = (($n * _pow($x, 1 - $exp)) / $exp) - ($x / $exp) + $x; do { $x = $y; $y = (($n * _pow($x, 1 - $exp)) / $exp) - ($x / $exp) + $x; } while ($x > $y); return $result * $x; // 4^2.5 = 4^2 * 4^0.5 = 16 * 2 = 32 } return $result; } ``` The above seems to work great except when `1 / fmod($exp, 1)` doesn't yield an integer. For example, if `$exp` is `0.123456`, its inverse will be `8.10005` and the outcome of `pow()` and `_pow()` will be a bit different (demo): - `pow(2, 0.123456)` = `1.0893412745953` - `_pow(2, 0.123456)` = `1.0905077326653` - `_pow(2, 1 / 8)` = `_pow(2, 0.125)` = `1.0905077326653` How can I achieve the same level of accuracy using "manual" exponential calculations?

Original source

Related problems