log 在 PHP 和 Lua 中是非单调的

Lobsters Hottest 新闻

摘要

PHP 和 Lua 的 log 函数由于对以 10 为底(log10)和其他底数(ln)使用不同算法,可能产生非单调的结果,导致出现违反直觉的行为,即底数越大,对数值反而越大。

<p><a href="https://lobste.rs/s/pa54mh/log_is_non_monotonous_php_lua">评论</a></p>
查看原文
查看缓存全文

缓存时间: 2026/07/22 12:21

# log 在 PHP 和 Lua 中是非单调的 来源:https://purplesyringa.moe/blog/log-is-non-monotonous-in-php-and-lua/ 2026 年 7 月 22 日 如果 \(a > b > 1\) 且 \(x > 1\),您可以证明 \(\log_a x < \log_b x\)。数值计算应该尊重这一点。但在 PHP 中: ```php $a = M_E; // 2.718281828459045 $b = 10; $x = 1000; var_dump($a > $b); // true var_dump($x > 1); // true var_dump(log($x, $a)); // float(6.9077552789821) var_dump(log($x, $b)); // float(3.0) var_dump(log($x, $a) > log($x, $b)); // false var_dump(log($x, $a) < log($x, $b)); // false var_dump(log($x, $a) == log($x, $b)); // true ``` 需要明确的是,这**不是**通常的浮点数不精确问题。每个人都知道浮点运算不精确,如果只是那样就不值得写博客了。这个例子是精心设计来触发不同情况的。如果产生的结果显示 \(\log_a x = \log_b x\) 而不是 <,那是完全自然的:并非所有实数都能精确表示为浮点数,舍入可能导致接近的结果看起来相等。事实上,对于同样的数字,Python 的 `math.log` 产生的是“等于”的结果。然而在 PHP 中,结果却以某种方式*反转*了。而且在 Lua 中也是!但在 Rust 或 C# 中却不是。全部都在同一台机器和操作系统上!怎么会这样? 另请注意,我只改变了底数:如果我也改变了参数,我会找到很多在任何语言中都表现异常的反例,例如: `log(243 ** 3, 3 ** 3) != log(243, 3)` ……因为计算 `log` 所用的公式并不完美。但这不是我们讨论的重点。 ### 对数底数 为了找出原因,让我们讨论一下语言通常如何实现 `math.log`。 `libm`(提供**超越函数**的库)公开了多个计算对数的函数:`log`、`log10`、`log2` 等。每个函数处理一个固定的底数:`log` 使用底数 e,`log10` 使用底数 10,等等。虽然这些函数的精度没有保证,但它们通常相当不错,并且至少是单调的(暴力验证)。但不存在任意底数的函数,因此提供双参数 `log` 的语言不得不取巧。 数学上,\(\log_a x = \frac{\ln x}{\ln a}\),因此你可以用两个以 e 为底的对数来计算任意对数。这样做不太精确,这就是为什么 `math.log(243^3, 3^3) == math.log(243, 3)` 会失败:因为 `ln` 和除法带来的双重舍入使得计算出的值略有偏差。但这并不能解释文章开头那个 x 不变的情况。在那个例子中,分子保持不变(\(\ln x\)),分母(至少从符号上看)变小了,但结果却以某种方式*变小*了?即使是浮点数,这也太奇怪了!更奇怪的是,如果你真的在 PHP 或 Lua 中*计算* \(\ln a\) 和 \(\ln b\),你会发现它们舍入后变成了相同的值!那么,分子和分母都没有改变,结果怎么会变化呢? ### 解决方案 你可能已经看出了原因。\(b = 10\) 是一个非常具体的反例,而在双 `log` 精度误差中存在一个可疑的 `log10` 形状的洞。PHP 和 Lua 并不*总是*使用 \(\frac{\ln x}{\ln a}\) 公式。对于那些 `libm` 直接实现的底数,即底数 10 和底数 2,它们会直接调用相应的函数(`log10` 和 `log2`),而不会经过自然对数。因此,问题中的代码并不是比较两个 \(\frac{\ln x}{\ln a}\) 的计算结果,而是比较 `log(x) / log(10 + eps)` 和 `log10(x)`。由于这些是完全不同的方法,它们可能产生不同方向的误差也就不足为奇了。初衷是好的:在适用的情况下,`log10` 能提供更精确、更快的结果。但将两种求值方法结合在一起,会在边界处产生不连续性,从而破坏了每种方法单独满足的合理假设! ### 闲谈 我不一定会称其为 bug,但它确实是一个被忽视的问题。不幸的是,这在浮点数世界中相当常见:IEEE 754 本身非常健壮,但这里那里不经思考的实现决策污染了它,以至于人们几乎将所有浮点运算 bug 都归咎于固有的不精确性。 对于 Lua(它只提供 `math.log`,而不提供 `math.log10`),正确的做法应该是添加 `math.log10`,并从 `math.log` 中移除特殊处理。这样,使用固定底数 10 的人可以使用更快更简单的 `math.log10` 方法,该方法可以提供更好的精度边界,而使用可变底数的人则不必担心边缘情况。哦,等等,他们在 Lua 5.2 中几乎做了完全相反的事情!我喜欢人们重视正确性。 另一方面,PHP 尽管已经有了 `log10`,但仍然对底数 10 和 2 进行特殊处理,据说是为了那些不看手册的人——如果真有人疯狂到试图猜测 PHP 函数名,我倒不会低估他们的目标受众。我收到矛盾的信号,最好在头疼之前完成这篇文章。 有趣的是,PHP 和 C# 还对 `log(x, 1)` 进行特殊处理,返回 `NaN` 而不管 `x` 如何,而 Lua 和 Python 通常返回 \(\pm\infty\)。真是一个多样化的生态系统。 哦,还有:PHP 中 `log` 的函数签名说默认的 `base` 是 `M_E`(e 的*近似值*),但文档说没有底数的 `log` 返回自然对数。那么 `log(x)` 计算的是 \(\log_{\text{round}(e)} x\) 还是 \(\log_e x\) 呢?(这个问题比看上去更重要:例如,`sin(M_PI)` *正确地*返回非零值,因为 `M_PI` 并不是精确的 \(\pi\)。)Lua 至少还算体面地进行了模糊化处理。剧透:是后者,但实际上 `log(x, M_E)` 会得到同样的结果,因为 \(\frac{\ln x}{\ln a}\) 中的舍入误差足够大,使得两者重合。

相似文章

关于NaN的两个案例分析

Lobsters Hottest

本文探讨了NaN在Python和Lua中的两种意外行为,其中对相等性和比较的隐含假设导致了令人惊讶的结果,例如Python列表相等性忽略了NaN的自不等性,以及Lua的for循环将NaN作为步长或限制时处理不当。

PHP 的古怪特性

Hacker News Top

一位开发者在使用了五年后反思 PHP 的古怪之处,重点介绍了其数组实现和类型系统的奇特之处。

Lean软件规模定律(阅读时间17分钟)

TLDR AI

该研究提案探讨了不同编程语言中代码库大小如何影响编码LLM的困惑度,并以Lean作为形式语言的测试案例。它表明Lean可能具有更优的缩放指数,从而使大规模软件更安全、更可靠。

优化Lua字符串字面量以节省400字节

Hacker News Top

一篇技术博客文章,探讨在ComputerCraft程序环境下,通过选择最优表示方式(引号字符串与原始字符串)来减小Lua字符串字面量大小的方法,实现了400字节的节省。