Investigating non-monotonic logarithms in PHP and Lua (and the market for lemons in standard libraries)
PHP and Lua's broken logarithms are not unavoidable floating-point noise, but naive division hacks rewarded by a market that ignores invisible regressions.
By Wren Okada
Sparked by Log is non-monotonic in PHP and Lua · discussion

When a recent post on purplesyringa.moe pointed out that logarithms in PHP and Lua contain bizarre mathematical discontinuities where the output of log(x) actively decreases as the input x increases, the cocktail-party consensus in the resulting Hacker News discussion thread overwhelmingly dismissed the issue. Commenters repeatedly argued that non-monotonicity is simply an unavoidable consequence of inherent floating-point imprecision and the Table Maker's Dilemma, treating localized mathematical inversions as a fundamental law of computing that developers just have to accept. The most highly upvoted responses essentially told the researcher to go read "What Every Computer Scientist Should Know About Floating-Point Arithmetic" and stop complaining about a system behaving exactly as designed.
That is mathematically false.
Any engineer looking at this should immediately recognize the severity of the failure, but we don't have to rely on intuition because we can look at the raw output of the benchmarking script compiled by the original researcher, which fuzzes standard library log() functions across fifteen different programming languages to measure basic mathematical monotonicity.
| Language | Monotonicity Preserved | Standard Library Backend Mechanism |
| :--- | :--- | :--- |
| Rust | Pass | Core library / LLVM intrinsics |
| Go | Pass | Custom math package implementation |
| Python | Pass | Wraps C math.h (but uses fdlibm fallbacks historically) |
| Java | Pass | StrictMath (fdlibm) or intrinsic |
| C# | Pass | CoreCLR custom implementation |
| JavaScript (V8) | Pass | V8 custom torque / intrinsics |
| Ruby | Pass | Standard C math with internal numeric tower |
| Swift | Pass | Platform specific / LLVM |
| Kotlin | Pass | JVM Math delegation |
| Scala | Pass | JVM Math delegation |
| Haskell | Pass | GHC internal math bindings |
| Zig | Pass | Custom standard library math |
| Nim | Pass | Custom standard library math |
| PHP | Fail | Custom C-source special-casing |
| Lua | Fail | Custom C-source special-casing |
Thirteen out of fifteen standard libraries evaluated correctly maintain perfect monotonicity under extreme precision testing, which puts any argument that this is an unavoidable side effect of the IEEE 754 floating-point specification in a very difficult position.
To understand the exact boundary failure condition where the math completely breaks down in the failing languages, we can look at the evaluated base of x = 2.93. When testing this base, the script feeds the logarithm function two inputs: a = 10 + 2^-49 and b = 10. Because 10 + 2^-49 is strictly greater than 10, any correct mathematical function guarantees that the output of log(a) must be greater than or equal to log(b). But if we walk through the raw arithmetic output in PHP and Lua, the outputs actively invert, meaning the host runtime evaluates log(10 + 2^-49) as a smaller floating-point value than log(10).
If we pull down the source tree and grep through the C-source code to figure out why two structurally different languages share the exact same logic anomaly, we find that the maintainers explicitly authored a hardcoded special-casing block to handle arbitrary bases rather than using a numerically stable algorithm. If you open the raw source for Lua's lmathlib.c (specifically the math_log function) or PHP's equivalent ext/standard/math.c, you will see a manual branching structure. The logic basically checks if the user provided a specific base, and if it happens to be 10.0 or 2.0, it hits a fast path calling the host's native log10() or log2(). But if the base is arbitrary, like our 2.93, it falls through to a hardcoded division fallback: return log(x) / log(base);.
This is an ops smell.
This manual division of two separate, independent floating-point approximations is what actively destroys the monotonicity. Because the host's log(x) function already contains a small fractional error margin, dividing it by another approximated constant log(base) compounds the truncation error at the 53rd bit of the mantissa, occasionally causing the output derivative to flip negative. The developers manually introduced this instability by hacking around the base conversions with naive division rather than implementing a proper change-of-base polynomial that respects floating-point boundaries.
From an economic perspective, software ecosystems often degrade into a lemons market when users can't easily evaluate the structural integrity of the underlying dependencies.
When language adoption is driven by cocktail-party developer experience metrics—how fast the package manager resolves dependencies, the syntactic sugar for async routines, or raw HTTP throughput in synthetic benchmarks—maintainers have absolutely zero economic or reputational incentive to spend their limited complexity budget on writing a mathematically perfect, custom log() function. If a language core team spends five hundred hours porting and verifying a pristine implementation of fdlibm [hover: "Freely Distributable Math Library, the gold standard for correctly rounded math written by Sun Microsystems in 1993"] to replace their hardcoded C branches, no user will notice the improvement because the failure rate of non-monotonic math errors only surfaces in extreme edge cases that users naturally attribute to their own application logic.
Because the market rewards visible local optimizations and ignores invisible global regressions, the economically rational move for any language maintainer is to keep the hacked-together return log(x) / log(base); branch, ship the runtime compiler slightly faster, and blame standard floating-point noise when users inevitably file bug reports about sorting algorithms crashing due to broken array bounds. In the 1990s, when a CPU pipeline stall on a complex math function could severely bottleneck a system running at 100MHz, shaving cycles off a logarithm computation by sacrificing sub-ulp accuracy and using a naive division hack was a defensible engineering compromise. Today, with processors featuring massive L1 caches and out-of-order execution, retaining this legacy optimization at the cost of breaking basic algorithmic properties is just a giant pile of money being lit on fire via lost developer debugging hours.
Appendix A: Why floating point noise is a garbage excuse for non-monotonicity
- The IEEE 754 specification explicitly guarantees exact rounding for basic arithmetic operations (addition, subtraction, multiplication, division, and square root). While transcendental functions like logarithms are excluded from the strict exact-rounding mandate due to the computational complexity of the Table Maker's Dilemma, it has been mathematically proven for decades that algorithms can be authored to respect rounding boundaries within
0.5 ulp, preserving strict monotonicity. Rust and Go simply choose to pay the minor complexity cost to guarantee this behavior, whereas PHP and Lua explicitly chose not to. - Even if we assume a baseline error distribution of
1 ulpacross a floating-point operation, localized inversions cannot be dismissed as random hardware noise when dealing with deterministic inputs. If you apply standard Chernoff bounds to the expected error rates of Taylor series expansions operating on 64-bit precision floats, the probability of an algorithmic error randomly manifesting as a perfectly reproducible, non-monotonic inversion precisely atx = 2.93across millions of iterations is statistically indistinguishable from zero, meaning the discontinuity is entirely caused by the hardcoded C-source division logic. - When developers defend these shortcuts by pointing to the CPU cycles saved, they are fundamentally misunderstanding modern hardware architecture. If you're benchmarking the nanoseconds saved by doing a raw division instead of a proper polynomial expansion on a modern NUMA architecture without pinning your IRQ affinity to the specific core handling the system call, your context-switching overhead and L3 cache misses are almost certainly consuming an order of magnitude more latency than you would spend just computing the mathematically correct logarithm in user space.
- I have no interest in dispensing sweeping advice on how to write a perfectly optimized C compiler, but I will point out the absurdity of worrying about a
20nsmath penalty in a language like PHP, where the runtime overhead of dynamically allocating memory for every single variable declaration inherently dominates the execution profile anyway. - If you look at the mailing list archives for the GNU C library (
glibc), which provides the underlyinglog()implementations that PHP and Lua are blindly dividing, the maintainers are completely transparent about the fact that they do not guarantee monotonicity for all transcendental functions. The manual's page on Errors in Math Functions explicitly admits that its functions routinely deviate by severalulp. Combining these known-inaccurate primitives with a hardcoded division operation and expecting monotonic output is a textbook example of cargo-cult programming at the language-design level.