首页 > 后端开发 > php教程 > BCMATH,PHP中的固定点数学,精确损失案例

BCMATH,PHP中的固定点数学,精确损失案例

Joseph Gordon-Levitt
发布: 2025-02-20 09:17:10
原创
498 人浏览过

Fixed Point Math in PHP with BCMath, precision loss cases

PHP 和 MySQL 固定点数值运算的挑战与技巧

处理固定点数值时,需要格外小心,尤其是在使用 PHP 和 MySQL 进行开发时。本文将探讨使用 PHP BCMath 扩展、MySQL 固定点表达式处理以及将固定点数据从 PHP 持久化到 MySQL 时遇到的障碍和细节问题。尽管存在一些挑战,我们将尝试找出如何处理固定点数值并避免精度损失的方法。

要点总结

  • PHP 中的 BCMath 扩展支持任意精度数学运算,但如果向其函数传递数值型变量,则可能导致精度损失。应改用表示数字的字符串值以避免此问题。
  • MySQL 支持固定点数字表达式,但是,如果任何操作数采用指数格式或字符串格式,则会被视为浮点数。PHP PDO 扩展没有用于绑定的 Decimal 参数类型,这可能导致结果不精确。
  • 为了在 PHP MySQL 应用程序中执行精确的数学运算,所有运算都可以在 PHP 中处理,并且只有使用 INSERT 或 UPDATE 语句才能将数据持久化到 MySQL。或者,可以手动构建 SQL 查询,确保所有 SQL 数学表达式都以十进制数表示。

BCMath 的问题

BCMath 文档指出:

为了进行任意精度数学运算,PHP 提供了二进制计算器,它支持任何大小和精度的数字,这些数字以字符串表示。

因此,BCMath 函数参数应以字符串表示。将数值型变量传递给 bcmath 函数可能导致错误的结果,与将双精度值视为字符串时出现的精度损失相同。

案例 1

echo bcmul(776.210000, '100', 10) . PHP_EOL;
echo bcmul(776.211000, '100', 10) . PHP_EOL;
echo bcmul(776.210100, '100', 10) . PHP_EOL;

echo bcmul(50018850776.210000, '100', 10) . PHP_EOL;
echo bcmul(50018850776.211000, '100', 10) . PHP_EOL;
echo bcmul(50018850776.210100, '100', 10) . PHP_EOL;
登录后复制
登录后复制

结果是:

<code>77621.00
77621.100
77621.0100
5001885077621.00
5001885077621.100
5001885077621.00 //此处可见精度损失</code>
登录后复制
登录后复制

切勿将数值型变量传递给 BCMath 函数,只能传递表示数字的字符串值。即使不处理浮点数,BCMath 也会输出奇怪的结果:

案例 2

echo bcmul('10', 0.0001, 10) . PHP_EOL;
echo bcmul('10', 0.00001, 10) . PHP_EOL;
echo 10*0.00001 . PHP_EOL;
登录后复制
登录后复制

结果是:

<code>0.0010
0 // 这真的很奇怪!!!
0.0001</code>
登录后复制

其原因是 BCMath 将其参数转换为字符串,并且在某些情况下,数字的字符串表示形式具有指数表示法。

案例 3

echo bcmul('10', '1e-4', 10) . PHP_EOL; // 也输出 0
登录后复制

PHP 是一种弱类型语言,在某些情况下,无法严格控制输入——希望处理尽可能多的请求。

例如,我们可以通过应用 sprintf 转换来“修复”案例 2案例 3

$val = sprintf("%.10f", '1e-5');
echo bcmul('10', $val, 10) . PHP_EOL;
// 给我们 0.0001000000
登录后复制

但是,应用相同的转换会破坏案例 1 的“正确”行为:

$val = sprintf("%.10f", '50018850776.2100000000');
echo bcmul('10', $val, 10) . PHP_EOL;
echo bcmul('10', 50018850776.2100000000, 10) . PHP_EOL;
500188507762.0999908450 // 错误
500188507762.10 // 正确
登录后复制

因此,sprintf 解决方案不适用于 BCmath。假设所有用户输入都是字符串,我们可以实现一个简单的验证器,捕获所有指数表示法的数字并正确转换它们。此技术在 php-bignumbers 中已实现,因此我们可以安全地传入类似 1e-20 和 50018850776.2101 的参数,而不会丢失精度。

BCMath 最终指导原则

切勿在 BCMath PHP 扩展函数中将浮点数用作固定点运算参数。仅使用字符串。

使用 BCMath 扩展运算时,请注意指数表示法的参数。BCMath 函数无法正确处理指数参数(例如“1e-8”),因此应手动转换它们。注意,不要使用 sprintf 或类似的转换技术,因为这会导致精度损失。

可以使用 php-bignumbers 库,它可以处理指数形式的输入参数,并为用户提供固定点数学运算函数。但是,其性能不如 BCMath 扩展,因此它是在健壮的包和性能之间的一种折衷方案。

MySQL 和固定点数字

在 MySQL 中,固定点数字使用 DECIMAL 列类型处理。您可以阅读 MySQL 官方文档了解数据类型和精确数学运算。

最有趣的部分是 MySQL 如何处理表达式:

数值表达式的处理取决于表达式包含的值的类型:

如果存在任何近似值,则表达式为近似值,并使用浮点运算进行计算。

如果不存在近似值,则表达式仅包含精确值。如果任何精确值包含小数部分(小数点后的值),则使用 DECIMAL 精确算术计算表达式,精度为 65 位数字。“精确”一词受限于可以在二进制中表示的内容。例如,1.0/3.0 可以用十进制表示法近似为 .333…,但不能写成精确数字,因此 (1.0/3.0)*3.0 不精确地计算为 1.0。

否则,表达式仅包含整数值。表达式是精确的,并使用整数算术进行计算,精度与 BIGINT 相同(64 位)。

如果数值表达式包含任何字符串,则将其转换为双精度浮点值,表达式为近似值。

这是一个简短的示例,演示了小数部分的情况:

echo bcmul(776.210000, '100', 10) . PHP_EOL;
echo bcmul(776.211000, '100', 10) . PHP_EOL;
echo bcmul(776.210100, '100', 10) . PHP_EOL;

echo bcmul(50018850776.210000, '100', 10) . PHP_EOL;
echo bcmul(50018850776.211000, '100', 10) . PHP_EOL;
echo bcmul(50018850776.210100, '100', 10) . PHP_EOL;
登录后复制
登录后复制

这看起来很简单,但让我们看看如何在 PHP 中处理它。

PHP 和 MySQL 中的精确数学运算

因此,现在我们必须将固定点值从 PHP 持久化到 MySQL。正确的方法是在查询中使用预处理语句和占位符。然后我们进行参数绑定,一切都是安全可靠的。

<code>77621.00
77621.100
77621.0100
5001885077621.00
5001885077621.100
5001885077621.00 //此处可见精度损失</code>
登录后复制
登录后复制

当我们将值绑定到语句占位符时,我们可以通过 bindValue 的第三个参数指定其类型。可能的类型由常量 PDO::PARAM_BOOL、PDO::PARAM_NULL、PDO::PARAM_INT、PDO::PARAM_STR、PDO::PARAM_LOB 和 PDO::PARAM_STMT 表示。所以问题是 PHP PDO 扩展没有用于绑定的十进制参数类型。结果,查询中的所有数学表达式都被视为浮点表达式,而不是固定点表达式。

如果我们想利用预处理语句并使用固定点数字,最好的方法是在 PHP 中执行所有数学运算并将结果保存到 MySQL。

echo bcmul('10', 0.0001, 10) . PHP_EOL;
echo bcmul('10', 0.00001, 10) . PHP_EOL;
echo 10*0.00001 . PHP_EOL;
登录后复制
登录后复制

结论

我们得出以下结论:

  • 切勿在 BCMath PHP 扩展函数中将浮点数用作固定点运算参数。仅使用字符串。
  • BCMath 扩展不适用于指数表示法的字符串数字。
  • MySQL 支持固定点数字表达式,但所有操作数都必须采用十进制格式。如果至少有一个参数采用指数格式或字符串格式,则将其视为浮点数,表达式将作为浮点数进行计算。
  • PHP PDO 扩展没有 Decimal 参数类型,因此,如果您使用预处理语句并在包含固定点操作数的 SQL 表达式中绑定参数,则不会获得精确的结果。
  • 为了在 PHP MySQL 应用程序中执行精确的数学运算,您可以选择两种方法。第一种方法是在 PHP 中处理所有运算,并且只有使用 INSERT 或 UPDATE 语句才能将数据持久化到 MySQL。在这种情况下,您可以使用预处理语句和参数绑定。第二种方法是手动构建 SQL 查询(您仍然可以使用预处理语句,但必须自行转义参数),以便所有 SQL 数学表达式都以十进制数表示。

我个人最喜欢的方法是第一种:在 PHP 中进行所有数学运算。我同意 PHP 和 MySQL 可能不是需要精确数学运算的应用程序的最佳选择,但是,如果您选择了这种技术堆栈,那么了解如何正确处理它是很好的。

(由于篇幅限制,FAQs 部分被省略。如果需要,可以单独生成FAQs部分。)

以上是BCMATH,PHP中的固定点数学,精确损失案例的详细内容。更多信息请关注PHP中文网其他相关文章!

本站声明
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
作者最新文章
热门教程
更多>
最新下载
更多>
网站特效
网站源码
网站素材
前端模板