If
Yet if you ask PHP what it thinks, it will tell you you’re wrong in a couple rare cases:
<?php
$x = 2.93;
$a = 10 + 2 ** -49;
$b = 10;
assert($a > $b);
var_dump(log($x, $a) < log($x, $b));
var_dump(log($x, $a) == log($x, $b));
To be clear, this is not the usual floating-point inaccuracy. Everyone already knows floating-point operations are imprecise and it wouldn’t be fun to blog about. This example was deliberately engineered to trigger something different.
It would be completely natural if the produced result indicated math.log produces the “equals” result. In PHP, however, the result somehow flips. And in Lua, too! But not in Rust or C#. All on the same machine and OS! How come?
Also note that I’m only changing the base: if I also changed the argument, I would find lots of counterexamples that work in any language, e.g.:
log(243 ** 3, 3 ** 3) != log(243, 3)
…because the formula used for log is imperfect. That’s not what we’re discussing.
To find why this happens, let’s discuss how languages usually implement math.log.
libm, the library that provides transcendental functions, exposes multiple functions for computing logarithms: log, log10, log2, etc. Each function handles a single base: log uses base log10 uses base
But there is no function for an arbitrary base, so languages providing a two-argument log have to cheat. Mathematically,
It’s a little imprecise, which is why math.log(243^3, 3^3) == math.log(243, 3) fails: double rounding from ln and the division makes the computed value slightly off.
But this doesn’t explain the case when
Even more weird is the fact that if you actually compute
You probably already see the reason. log10-shaped hole in the double-log precision error.
PHP and Lua don’t always use the libm implements directly, i.e. base log10 and log2) without going through the natural logarithm. So the code in question doesn’t compare two log(x) / log(10 + eps) to log10(x). Since these are completely different methods, it shouldn’t be a surprise that they may result in errors in different directions.
The intention is good: when applicable, log10 provides a more precise, faster result. But combining two evaluation methods results in a discontinuity at the boundary, which breaks reasonable assumptions that the methods satisfy separately!
I wouldn’t necessarily call this a bug, but it certainly is an overlooked issue. This is, unfortunately, quite common in the floating-point world: IEEE-754 itself is incredibly robust, but thoughtless implementation decisions here and there contaminate it to the point where people attribute just about any FP bug to intrinsic imprecision.
For Lua, which only provides math.log, but not math.log10, the right thing to do would be to add math.log10 and remove the special casing from math.log. This way, people who use fixed base math.log10 method, which could provide better precision boundaries, while people who use a variable base will benefit from not having an edge case to worry about. Oh wait, they did pretty much the exact opposite in Lua 5.2! I like it when people value correctness.
PHP, on the other hand, special-cases bases log10, supposedly for people who don’t RTFM, which I wouldn’t put past their target audience, if only anyone could be insane enough to try to guess PHP function names. I am getting mixed signals, so I’d better finish this post before I get a headache.
Curiously, PHP and C# also special-case log(x, 1) to return NaN regardless of x, while Lua and Python typically return
Oh, and also: PHP’s signature for log says the default base is M_E (the approximation of log without a base returns the natural logarithm. So does log(x) compute sin(M_PI) correctly returns a non-zero value, because M_PI is not exactly log(x, M_E) would produce the same result, because the rounding in