【问题标题】:Can Perl detect if a floating point number has been implicitly rounded?Perl 可以检测浮点数是否已被隐式舍入吗?
【发布时间】:2016-09-17 19:42:47
【问题描述】:

当我使用代码时:

(sub {
    use strict;
    use warnings;

    print 0.49999999999999994;
})->();

Perl 输出“0.5”。

当我从数字中删除一个“9”时:

(sub {
    use strict;
    use warnings;

    print 0.4999999999999994;
})->();

它打印 0.499999999999999。

只有当我删除另一个 9 时,它才会精确地存储数字。

我知道浮点数是一堆没人愿意处理的蠕虫,但我很好奇 Perl 中是否有办法“捕获”这种隐式转换并死掉,这样我就可以使用 eval 来捕获它死并让用户知道 Perl 不支持他们试图传递的数字的本机形式(因此用户可以传递一个字符串或一个对象)。

我需要这个的原因是为了避免像传递 0.49999999999999994 被我的函数四舍五入这样的情况,但是数字被转换为 0.5,然后被四舍五入为 1 而不是 0。我不知道如何“拦截”这个转换,以便我的函数“知道”它实际上并没有得到 0.5 作为输入,而是拦截了用户的输入。

在不知道如何拦截这种转换的情况下,我不能相信“round”,因为我不知道它是否在我发送它时收到了我的输入,或者该输入是否已被修改(在编译时或运行时,不确定) 在函数被调用之前(反过来,函数不知道它正在操作的输入是否是用户想要的输入,并且无法警告用户)。

这不是 Perl 独有的问题,它发生在 JavaScript 中:

(() => {
    'use strict';

    /* oops: 1 */
    console.log(Math.round(0.49999999999999999))
})();

它发生在 Ruby 中:

(Proc.new {
    # oops: 1 
    print (0.49999999999999999.round)
}).call()

它发生在 PHP 中:

<?php
(call_user_func(function() {
    /* oops: 1 */
    echo round(0.49999999999999999);
}));
?>

它甚至发生在 C 中(这是可以发生的,但是我的 gcc 并没有警告我该数字没有被精确存储(当指定特定的浮点文字时,它们最好准确存储,否则编译器应该发出警告你决定把它变成另一种形式(例如“你的数字 x 不能以 64 位/32 位浮点形式表示,所以我将它转换为 y。”)所以你可以看看这是否可以,在这个如果不是)):

#include <math.h>
#include <stdio.h>

int main(int argc, char **argv)
{
    /* oops: 1 */
    printf("%f.\n", round(0.49999999999999999));

    return 0;
}

总结

是否有可能让 Perl 在浮点数的隐式转换上显示错误或警告,或者这是 Perl5(以及其他语言)目前无法做到的事情(例如,编译器不会退出它的)支持此类警告的方式/提供启用此类警告的标志)?

例如

警告:数字 0.49999999999999994 不可表示,已转换为 0.5。使用 bigint 可能会解决这个问题。考虑降低数字的精度。

【问题讨论】:

  • 如果他们能把人送上月球……当然有可能,但这将是一个项目。考虑perl -le 'printf("%.18f\n", 0.49999999999999994-10)' 打印-9.500000000000000000,因此IEEE 754 将永远丢失微小的差异,除非您能抓住差异。我认为唯一的捷径是使用 BigNum 或 Math::BigFloat 并将 BigFloat 的完整表示与字符串中的内部 IEEE 754 表示进行比较。这是一个开始。
  • “我的 gcc 没有警告我这个数字没有被精确存储”
  • @Dmitry:浮点值的内部表示几乎总是与其字符串形式具有不同的值。 Perl 会在可能的情况下保留一个整数值,但是一旦你这样做了,比如除法,你可能会得到一个准确的结果 (27/3) 或一个不准确的结果 (2/3)。这仍然归结为您首先对此感兴趣的原因。正如Mark Dickinson suggests 一样,警告每一次违规都没有什么意义。
  • “只有当我删除另一个 9 时,它才会准确地存储数字” 不。只有当你删除那个 9 时,打印输出 等于输入字符串。浮点数对于绝大多数应用程序来说足够准确。我认为您更有可能需要更好地了解如何处理代码中的浮点数。 0.49999999999999994 是万亿分之一的精度。这是巨大的,是地球人口的 100,000 倍,你需要做一些专业的事情才能做到这一点。
  • 您的实际问题是什么?你认为你为什么需要这个?

标签: perl floating-point


【解决方案1】:

也许用BigNum:

$ perl -Mbignum -le 'print 0.49999999999999994'
0.49999999999999994
$ perl -Mbignum -le 'print 0.49999999999999994+0.1'
0.59999999999999994
$ perl -Mbignum -le 'print 0.49999999999999994-0.1'
0.39999999999999994
$ perl -Mbignum -le 'print 0.49999999999999994+10.1'
10.59999999999999994

它透明地将 Perl 浮点和整数的精度扩展到扩展精度。

【讨论】:

  • 这有帮助,它在内部是如何工作的?它似乎完全改变了处理 "(\d+)[.](\d+)" 终端的方式,而无需重新编译整个 Perl 解释器。此外,除了它更大之外,它是否也达到了隐式舍入的程度?不幸的是,它仍然存在这样一个问题,即如果用户尝试传递数字,除非用户也使用 bignum(不仅仅是 perl 模块),否则它仍然会四舍五入,除非我们信任用户阅读模块文档,否则这并不明显(这不符合最小惊讶原则)。
  • BigInt 是 GNU GMP 库的包装器。浮点数的表示方式肯定存在内部差异。也许是速度问题;当然是二进制交换问题。文本上的表示应该是相同的。
  • @Dmitry: bignum 使用了重载::constant 钩子metacpan.org/pod/overload#Overloading-Constants;你也可以自己做。
  • bignum 似乎没有解决 OP 对浮点数精确表示的担忧。 perl -Mbignum -E 'printf "%0.30f\n", 0.2' 仍然打印 0.200000000000000011102230246252
  • @DaveSherohman BigNum 不是无限精度;它是扩展精度。如果您想控制您希望支付多少扩展精度(在速度和存储要求方面),您可以使用Math::BigFloat 并设置所需的精度。您使用的示例有其自身的问题:printf %0.30f" 导致转换为 IEEE 754 浮点数,这就是您看到伪像的原因。试试看:$ perl -Mbignum -E 'print 0.2*100000000000000000000000000000, "\n"'
【解决方案2】:

请注意,bignum 比内部和其他数学解决方案慢 150 倍,并且通常不会解决您的问题(只要您需要将数字存储在 JSON 或数据库或其他任何内容中,您就会回到同样的问题再次)。

通常,sprintf 会为您处理漂亮的输出,因此您不必看到丑陋的不精确,但是,它仍然存在。

这是一个适用于我的 x64 平台的示例,它了解如何处理这种不精确性。

这会正确地告诉您您感兴趣的 2 个数字是否相同:

sub safe_eq {
  my($var1,$var2)=@_;
  return 1 if($var1==$var2);
  my $dust;
  if($var2==0) { $dust=abs($var1); }
  else { $dust= abs(($var1/$var2)-1); }
  return 0 if($dust>5.32907051820076e-15 ); # dust <= 5.32907051820075e-15 
  return 1;
}

您可以在此基础上解决所有问题。

它的工作原理是了解您的母语数字的不精确程度并加以调整。

【讨论】:

    【解决方案3】:

    正如您在问题中所说,在代码中处理浮点数是非常容易的,正是因为标准浮点表示,无论采用何种精度,都无法准确表示许多十进制数。唯一 100% 可靠的解决方法是不使用浮点数

    最简单的应用方法是改用定点数,尽管这会将精度限制为固定的小数位数。例如,不存储 10.0050,而是定义一个约定,将所有数字存储到小数点后 4 位,并存储 100050。

    但这似乎不太可能让您满意,根据您对实际尝试完成的任务(构建通用数学库)给出的最低限度的解释。那么,下一个选项是将小数位数存储为每个值的比例因子。所以 10.0050 将成为一个包含数据 { value =&gt; 100050, scale =&gt; 4 } 的对象。

    然后可以通过将每个数字有效地存储为分子和分母来将其扩展为更通用的“有理数”数据类型,从而允许您精确存储诸如 1/3 之类的数字,这既不是以 2 为底,也不是以 10 为底准确地代表。顺便说一下,我听说 Perl 6 采用了这种方法。因此,如果切换到 Perl 6 是一种选择,那么一旦您这样做,您可能会发现这一切都对您有效。

    【讨论】:

    • 我不想构建一个通用库,但我想为 Perl 数学库做一个包装器,使其与 JavaScript 的 Math 对象完全一样。一切似乎都很简单,直到我遇到 Perl 没有易于访问的“round”函数的问题,所以我尝试编写自己的并意识到 round 根本没有信息来检查它是否在做正确的事情,因为浮动像 0.49.... 被转换为 0.5 而不是 0.4,从而打破了 round 的不变量,而 round 无法知道它。我想知道是否有聪明的方法可以让回合恢复。
    • 如果不使 Math 库不再具有与 JavaScript Math 对象相同的接口,我将无法转换到另一个模型。同时,如果我坚持界面,我无法检测用户输入的 0.4999... 是否已转换为 0.5。如果我可以强制此特定函数将浮点输入捕获为字符串(如果它是直接输入),那将不是问题,这样该回合就可以检查它是否在做正确的事情。然而,这似乎也不可能;我必须明确强制用户传递一个字符串或一个非浮点对象作为输入来保护它们。
    • 如果我的库最终被用于重要的事情(不太可能,但如果),并且不知道浮点如何工作的人输入 0.499999.. 由于某种原因被四舍五入,我的库不能纠正这个看似微不足道的转换:它不知道转换已经发生,也不知道实际的用户输入字符串在转换为适合双精度之前是什么。
    • @Dmitry:您可以控制舍入。使用Math::BigFloat rounding features...
    • 是的,您需要使用这些软件包之一,否则您将获得默认行为。或者自己写。或者,使用 POSIX 模块中的 round。或者,使用 cpan 中的 Math::Round。 Tim Toady 有一个 cheat sheet 可能会有所帮助。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2020-09-22
    • 2022-08-18
    • 1970-01-01
    • 2011-07-10
    • 1970-01-01
    • 2016-04-21
    • 2020-06-09
    相关资源
    最近更新 更多