Why log is Non-Monotonic in PHP and Lua Due to Hidden Implementation Tricks

I discovered that the log function in PHP and Lua breaks mathematical monotonicity in rare cases. This happens because these languages switch between different calculation methods for specific bases like 10 and 2, creating a discontinuity. While other languages like Python and Rust handle this consistently, PHP and Lua's optimization choices lead to unexpected results that confuse developers relying on standard floating-point behavior.

I like it when people value correctness.
  1. ReactiveJelly

    > Everyone already knows floating-point operations are imprecise and it wouldn’t be fun to blog about.

    Wellll... Yes and no. And I want to nitpick "FP ops are imprecise" because it's important sometimes.

    It does come up in Lua - Lua uses 64-bit double floats for everything, _even array indexing_, because they have 53 bits of mantissa and they're guaranteed to represent all 32-bit integers with 100% precision.

    I just opened Lua and got `2 ^ 32 == 4294967296.0` and `2 ^ 32 + 1 == 4294967297.0`. You can store 4 billion things in a Lua table and access them with double float indexes.

    The usual "0.1 + 0.2 != 0.3" is _not_ imprecision. You could dedicate 1,000 bits to a float and still find some example of a number that's trivial to represent in decimal but repeats forever in binary.

    While I'm on it, fixed-point and floating-point aren't magically different. Floats work better on very big values. Fixed-points are more predictable but run out of range easily when squaring numbers. This comes in 3D math when finding the length of vectors or normalizing vectors.

    The PlayStation 1 didn't have jiggly vertices and warpy textures because of fixed-point. It had jiggly vertices because its GPU didn't have subpixel-precise rendering, and it only had 16 bits of precision. It had warpy textures because it had affine texture mapping. The N64's GPU had more bits of precision and it had perspective-correct texture mapping. There's no 16-bit floating-point format that would have saved the PS1 fro […]

  2. codeflo

    There's a widespread misconception (not shared by the article author) that floating point arithmetic is "imprecise" in the sense that the result is off by some amount of random noise. On the contrary, IEEE floating point results are precisely specified to produce the closest representable value to the mathematically exact result.

    For efficiency reasons, library functions (and sometimes, sadly, hardware implementations) don't always guarantee this (closest representable) exact result. That's fine if the function is then called "approximate_inverse_square_root" or something. Sometimes being off by some epsilon is a good tradeoff for 10x efficiency. I'm unhappy when such a tradeoff is smuggled into my math library without warning.

    I checked Rust's source code to confirm the article's claim that it doesn't have a precise log function, and indeed, here's the implementation:

    pub fn log(self, base: f64) -> f64 {

    self.ln() / base.ln()

    }

    I'm sure other math libraries aren't better. But this is not a correct implementation of arbitrary-base logarithm. A function like that perhaps shouldn't even be offered in the standard library at all (since it's so trivial to begin with), or at the very least, not with that name. If a programmer wants to opt-in to a fast but slightly wrong value, they should do so explicitly, in my opinion.

  3. kstrauser

    I was prepared to read how its console logging function could reorder writes so that

    log('a');

    log('b');

    could result in

    b

    a

    or something, which would've been no less surprising.

  4. cwt137

    I find it interesting the blog author tied PHP and Lua together. PHP uses a JIT from Lua. Is this related to the log issue?

  5. lioeters

    Recently I was reading an article on the EML operator (exp-min-log) that uses exponentiation and logarithm to build elementary math functions including arithmetic operations. There was a table of results from testing across languages.

    Language Result of 2 x 3 Error

    ---

    Node.js v25.3.0 6.000000000000000 0

    Python 3.9.6 6.000000000000001 8.88e-16

    PHP 8.5.1 6.000000000000001 8.88e-16

    Go 1.26.2 6.000000000000000 0

    Rust 6.000000000000001 8.88e-16

    It was speculated that this miniscule margin of error, 1 ULP (unit in the last place), is likely due to the difference in how log() is implemented by the language. Supposedly Python, PHP, and Rust use LLVM's libm (C math library) but maybe Go and Node.js internally compile to CPU instructions directly? There was no evidence presented, so I was skeptical of this explanation.

More from this day

2026-07-29