比较浮点值有多危险?

How dangerous is it to compare floating point values?

提问人:Proud Member 提问时间:4/26/2012 最后编辑:Nitin GohelProud Member 更新时间:9/2/2022 访问量:99603

问:

我知道用途是因为分辨率无关的坐标系。UIKitCGFloat

但是每次我想检查时,例如,它是否让我感到恶心:frame.origin.x0

if (theView.frame.origin.x == 0) {
    // do important operation
}

与 、 、 、 ? 例如,它是一个浮点数,它们存在不精确的问题。CGFloat==<=>=<>0.0000000000041

在比较时是在内部处理这个问题,还是会发生读数为零的情况与真值不比较的情况?Objective-Corigin.x0

Objective-C IOS C 点浮 点精度

评论

1赞 Hampus 1/23/2021
这主要是非整数值的问题,其中很容易发生舍入错误。写了一篇博客文章,描述了舍入错误何时发生以及如何估计潜在错误的大小。

答:

12赞 GoZoner 4/26/2012 #1

[“正确答案”比选择更胜一筹。选择最终与选择一样是临时的,但选择不太明显,因为与选择不同,它不基于任何显示属性。因此,选择你的毒药 - 选择或选择.这个答案提倡选择,然后论证了选择的难度KKVISIBLE_SHIFTKVISIBLE_SHIFTKVISIBLE_SHIFTVISIBLE_SHIFTK]

正是由于舍入误差,您不应该使用“精确”值的比较进行逻辑运算。在视觉显示器上的位置的特定情况下,位置是 0.0 还是 0.0000000003 都无关紧要 - 差异是肉眼看不见的。所以你的逻辑应该是这样的:

#define VISIBLE_SHIFT    0.0001        // for example
if (fabs(theView.frame.origin.x) < VISIBLE_SHIFT) { /* ... */ }

但是,最终,“肉眼看不见”将取决于您的显示属性。如果可以上限显示(应该可以);然后选择成为该上限的一小部分。VISIBLE_SHIFT

现在,“正确答案”取决于,所以让我们探索一下采摘。上面的“正确答案”说:KK

K 是您选择的常数,使得 计算肯定以最后 K 个单位为界(和 如果您不确定是否正确了误差边界计算,请将 K 设为 比你的计算结果大几倍)

所以我们需要.如果获得比选择我的更困难,更不直观,那么您将决定什么适合您。为了找到答案,我们将编写一个测试程序来查看一堆值,以便我们可以看到它的行为方式。应该很明显如何选择,如果“正确答案”可用。不?KKVISIBLE_SHIFTKKK

我们将使用,作为“正确答案”的详细信息:

if (fabs(x-y) < K * DBL_EPSILON * fabs(x+y) || fabs(x-y) < DBL_MIN)

让我们尝试 K 的所有值:

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

void main (void)
{
  double x = 1e-13;
  double y = 0.0;

  double K = 1e22;
  int i = 0;

  for (; i < 32; i++, K = K/10.0)
    {
      printf ("K:%40.16lf -> ", K);

      if (fabs(x-y) < K * DBL_EPSILON * fabs(x+y) || fabs(x-y) < DBL_MIN)
        printf ("YES\n");
      else
        printf ("NO\n");
    }
}
ebg@ebg$ gcc -o test test.c
ebg@ebg$ ./test
K:10000000000000000000000.0000000000000000 -> YES
K: 1000000000000000000000.0000000000000000 -> YES
K:  100000000000000000000.0000000000000000 -> YES
K:   10000000000000000000.0000000000000000 -> YES
K:    1000000000000000000.0000000000000000 -> YES
K:     100000000000000000.0000000000000000 -> YES
K:      10000000000000000.0000000000000000 -> YES
K:       1000000000000000.0000000000000000 -> NO
K:        100000000000000.0000000000000000 -> NO
K:         10000000000000.0000000000000000 -> NO
K:          1000000000000.0000000000000000 -> NO
K:           100000000000.0000000000000000 -> NO
K:            10000000000.0000000000000000 -> NO
K:             1000000000.0000000000000000 -> NO
K:              100000000.0000000000000000 -> NO
K:               10000000.0000000000000000 -> NO
K:                1000000.0000000000000000 -> NO
K:                 100000.0000000000000000 -> NO
K:                  10000.0000000000000000 -> NO
K:                   1000.0000000000000000 -> NO
K:                    100.0000000000000000 -> NO
K:                     10.0000000000000000 -> NO
K:                      1.0000000000000000 -> NO
K:                      0.1000000000000000 -> NO
K:                      0.0100000000000000 -> NO
K:                      0.0010000000000000 -> NO
K:                      0.0001000000000000 -> NO
K:                      0.0000100000000000 -> NO
K:                      0.0000010000000000 -> NO
K:                      0.0000001000000000 -> NO
K:                      0.0000000100000000 -> NO
K:                      0.0000000010000000 -> NO

啊,所以如果我希望 1e-13 为“零”,K 应该是 1e16 或更大。

所以,我想说你有两个选择:

  1. 正如我所建议的那样,使用您对“epsilon”值的工程判断进行简单的 epsilon 计算。如果你正在做图形,而“零”意味着一个“可见的变化”,那么检查你的视觉资产(图像等)并判断epsilon可以是什么。
  2. 在你阅读了非货物崇拜答案的参考文献(并在此过程中获得了博士学位)之前,不要尝试任何浮点计算,然后使用你的非直觉判断来选择 .K

评论

10赞 Romain 4/26/2012
分辨率独立性的一个方面是,在编译时无法确定什么是“可见偏移”。在超高清屏幕上看不见的东西在小屁股屏幕上可能很明显。至少应该让它成为屏幕尺寸的函数。或者给它起个别的名字。
2赞 GoZoner 8/28/2016
但至少选择“可见位移”是基于易于理解的显示(或框架)属性——不像<正确答案>选择困难且不直观。K
41赞 High Performance Mark 4/26/2012 #2

由于 0 可以完全表示为IEEE754浮点数(或使用我曾经使用过的任何其他 f-p 数实现),因此与 0 进行比较可能是安全的。但是,如果您的程序计算出一个值(例如),您有理由相信该值应该是 0,但您的计算不能保证该值为 0,则您可能会被咬。theView.frame.origin.x

澄清一下,这样的计算:

areal = 0.0

将(除非您的语言或系统被破坏)创建一个值,使得 (areal==0.0) 返回 true,但另一个计算,例如

areal = 1.386 - 2.1*(0.66)

可能不会。

如果你能确信你的计算产生的值是 0(而不仅仅是它们产生的值应该是 0),那么你可以继续将 f-p 值与 0 进行比较。如果你不能保证自己达到所需的程度,最好坚持通常的“宽容平等”方法。

在最坏的情况下,不小心比较f-p值可能是非常危险的:想想航空电子设备、武器制导、发电厂操作、车辆导航,以及几乎所有计算与现实世界相遇的应用。

对于愤怒的小鸟来说,没那么危险。

评论

13赞 Pascal Cuoq 8/7/2013
实际上,这是一个表达式的完美示例,如果您的编译器实现 IEEE 754,则其计算结果显然为 0.0,因为表示为并具有相同有效数的双精度,并且乘以 2 显然是精确的。1.30 - 2*(0.65)0.651.30
8赞 High Performance Mark 12/16/2013
仍然从这个中得到代表,所以我更改了第二个示例片段。
506赞 R.. GitHub STOP HELPING ICE 4/26/2012 #3

首先,浮点值的行为不是“随机的”。精确的比较在许多实际使用中是有意义的。但是,如果您要使用浮点,则需要了解它的工作原理。错误地假设浮点像实数一样工作,会让你的代码很快中断。假设浮点结果具有与之相关的大量随机模糊(就像这里的大多数答案所建议的那样)的错误将使您的代码一开始看起来有效,但最终会出现大幅度错误和损坏的极端情况。

首先,如果你想用浮点数编程,你应该阅读以下内容:

每个计算机科学家都应该了解的浮点运算知识

是的,阅读所有内容。如果负担太大,您应该使用整数/定点进行计算,直到您有时间阅读它。:-)

现在,话虽如此,精确浮点比较的最大问题归结为:

  1. 事实上,您可能在源中写入或用 或 读入的许多值不作为浮点值存在,而是以静默方式转换为最接近的近似值。这就是demon9733的答案。scanfstrtod

  2. 事实上,由于没有足够的精度来表示实际结果,许多结果被舍入。您可以看到这一点的一个简单的示例是添加 和 作为浮点数。在这里,尾数有 24 位精度(正常),只有 1 位,但是当您添加它们时,它们的位不在重叠的位置,结果需要 25 位的精度。相反,它会四舍五入(在默认舍入模式下)。x = 0x1fffffey = 1xy0x2000000

  3. 事实上,由于需要无限多的位置来获得正确的值,许多结果都会四舍五入。这既包括像 1/3 这样的理性结果(你从十进制中熟悉它,它占据了无限多位),也包括 1/10(它在二进制中也占据了无限多的位置,因为 5 不是 2 的幂),以及非理性结果,如任何不是完美正方形的东西的平方根。

  4. 双舍五入。在某些系统(尤其是 x86)上,浮点表达式的计算精度高于其标称类型。这意味着,当发生上述类型的舍入时,您将获得两个舍入步骤,首先是将结果舍入为更高精度的类型,然后是舍入到最终类型。例如,考虑一下如果将 1.49 四舍五入为整数 (1),则在小数点后会发生什么情况,如果先将其四舍五入到小数点后一位 (1.5),然后将结果四舍五入为整数 (2)。这实际上是浮点运算中最令人讨厌的领域之一,因为编译器的行为(尤其是对于像 GCC 这样有缺陷的、不合规的编译器)是不可预测的。

  5. 超越函数(、、等)未指定具有正确舍入的结果;结果只是在精度的最后一个位置(通常称为 1ULP)的一个单位内指定为正确的。trigexplog

当你编写浮点代码时,你需要记住你对可能导致结果不准确的数字做了什么,并相应地进行比较。通常,与“epsilon”进行比较是有意义的,但该 epsilon 应该基于您正在比较的数字的大小,而不是绝对常数。(在绝对常数 epsilon 可以工作的情况下,这强烈表明定点而不是浮点是完成这项工作的正确工具!

编辑:具体而言,相对于量级的 epsilon 检查应如下所示:

if (fabs(x-y) < K * FLT_EPSILON * fabs(x+y))

其中常数 from(用 fors 或 for s 替换)并且是您选择的常数,这样计算的累积误差肯定由最后的单位限制(如果您不确定自己的误差边界计算正确,请比计算结果大几倍)。FLT_EPSILONfloat.hDBL_EPSILONdoubleLDBL_EPSILONlong doubleKKK

最后,请注意,如果您使用它,可能需要在接近零时特别小心,因为对于异常没有意义。一个快速的解决方法是让它:FLT_EPSILON

if (fabs(x-y) < K * FLT_EPSILON * fabs(x+y) || fabs(x-y) < FLT_MIN)

如果使用双打,同样可以替换。DBL_MIN

评论

26赞 Daniel Fischer 4/26/2012
fabs(x+y)如果 和 (can) 具有不同的符号,则有问题。尽管如此,这还是一个很好的答案,可以对抗货物崇拜的比较浪潮。xy
31赞 R.. GitHub STOP HELPING ICE 4/26/2012
如果和有不同的标志,那就没问题了。右边会“太小”,但由于和符号不同,它们无论如何都不应该相等。(除非它们小到不正常,但随后第二种情况抓住了它)xyxy
6赞 Nicolás Ozimica 4/27/2012
我很好奇你的说法:“特别是对于像 GCC 这样有缺陷、不合规的编译器”。GCC 真的有问题并且不合规吗?
3赞 Stephen Canon 5/11/2012
由于这个问题被标记为 iOS,值得注意的是,Apple 的编译器(clang 和 Apple 的 gcc 构建)一直使用 FLT_EVAL_METHOD = 0,并试图完全严格地不携带过多的精度。如果您发现任何违规行为,请提交错误报告。
21赞 Christian Rau 7/6/2013
首先,浮点值的行为不是”随机“的。精确的比较在许多现实世界的用法中是有意义的,而且确实有意义。- 只有两句话,已经获得了+1!这是人们在使用浮点时最令人不安的错误假设之一。
-7赞 John White 5/3/2012 #4

我认为正确的做法是将每个数字声明为一个对象,然后在该对象中定义三件事:1)相等运算符。2) setAcceptableDifference 方法。3)价值本身。如果两个值的绝对差小于设置为可接受的值,则相等运算符返回 true。

您可以对对象进行子类化以适合该问题。例如,如果直径相差小于 0.0001 英寸,则 1 到 2 英寸之间的金属圆棒可以被视为直径相等。因此,您将使用参数 0.0001 调用 setAcceptableDifference,然后放心地使用相等运算符。

评论

2赞 Tom Swirly 5/15/2012
这不是一个好的答案。首先,整个“对象”对解决你的问题没有任何作用。其次,你对“平等”的实际实施实际上并不是正确的。
3赞 John White 8/17/2012
汤姆,也许你会再想一想“对象的事情”。对于以高精度表示的实数,相等很少发生。但是,如果一个人的平等理念适合你,它可能是量身定制的。如果有一个可重写的“近似相等”运算符会更好,但事实并非如此。
4赞 Lucas Membrane 5/14/2012 #5

上次我检查 C 标准时,对双精度(总共 64 位,53 位尾数)的浮点运算没有要求精确到超过该精度。但是,某些硬件可能会在更高精度的寄存器中执行操作,并且该要求被解释为不需要清除低阶位(超出加载到寄存器中的数字的精度)。因此,您可能会得到意想不到的比较结果,具体取决于最后睡在那里的人在寄存器中留下的东西。

话虽如此,尽管每当我看到它时,我都会努力删除它,但我工作的机构有很多使用 gcc 编译并在 linux 上运行的 C 代码,我们已经很久没有注意到这些意想不到的结果了。我不知道这是因为 gcc 正在为我们清除低阶位,80 位寄存器在现代计算机上不用于这些操作,标准已更改,还是什么。我想知道是否有人可以引用章节和经文。

15赞 JHumphrey 5/14/2012 #6

只要零不是计算值(如上面的答案中所述),与零进行比较就可能是一种安全的操作。这样做的原因是零是浮点数中完全可表示的数字。

说到完全可表示的值,您可以在 2 次幂概念(单精度)中获得 24 位范围。所以 1、2、4 是完全可以表示的,.5、.25 和 .125 也是如此。只要你所有的重要位都在 24 位,你就是黄金。因此,可以精确地表示 10.625。

这很好,但在压力下很快就会分崩离析。我想到了两种情况: 1)涉及计算时。不要相信 sqrt(3)*sqrt(3) == 3。它不会是那样的。而且它可能不会像其他一些答案所暗示的那样在epsilon中。 2) 当涉及任何非 2 次方 (NPOT) 时。所以这听起来可能很奇怪,但 0.1 是二进制中的无限级数,因此任何涉及此类数字的计算从一开始就是不精确的。

(哦,原来的问题提到与零的比较。不要忘记,-0.0 也是一个完全有效的浮点值。

24赞 starmole 5/14/2012 #7

我想给出一个与其他人不同的答案。它们非常适合回答您所说的问题,但可能不适合您需要知道的内容或您的真正问题是什么。

图形中的浮点很好!但是几乎没有必要直接比较浮点数。你为什么需要这样做?图形使用浮点数来定义间隔。比较浮点数是否在同样由浮点数定义的区间内总是定义得很好,只需要保持一致,而不是准确或精确!只要可以分配一个像素(这也是一个间隔!),这就是所有图形需求。

因此,如果您想测试您的点是否超出 [0..width[ 范围,这很好。只要确保你一致地定义包容性。例如,始终将 inside 定义为 (x>=0 && x < width)。交叉测试或命中测试也是如此。

但是,如果滥用图形坐标作为某种标志,例如查看窗口是否停靠,则不应这样做。请改用独立于图形表示层的布尔标志。

6赞 Michael T 5/7/2013 #8

正确的问题:如何在 Cocoa Touch 中比较点?

正确答案:CGPointEqualToPoint()。

另一个问题:两个计算值是否相同?

这里发布的答案是:他们不是。

如何检查它们是否接近?如果要检查它们是否接近,请不要使用 CGPointEqualToPoint()。但是,不要检查它们是否接近。做一些在现实世界中有意义的事情,比如检查一个点是否在一条线之外,或者一个点是否在球体内。

0赞 Igor 1/23/2017 #9

您可以使用以下代码将浮点数与零进行比较:

if ((int)(theView.frame.origin.x * 100) == 0) {
    // do important operation
}

这将与 0.1 的精度进行比较,在这种情况下,这对于 CGFloat 来说已经足够了。

评论

1赞 chux - Reinstate Monica 4/16/2017
在不投保的情况下进行强制转换在导致未定义行为 (UB) 的范围之内/附近 - 或者在本例中为 的 范围的 1/100。inttheView.frame.origin.xintint
1赞 Sneftel 12/14/2018
绝对没有理由像这样转换为整数。正如 chux 所说,超出范围的值有可能产生 UB;在某些架构上,这将比仅以浮点函数进行计算要慢得多。最后,像这样乘以 100 将比较 0.01 的精度,而不是 0.1。
-1赞 Abbas Mulani 3/13/2019 #10
-(BOOL)isFloatEqual:(CGFloat)firstValue secondValue:(CGFloat)secondValue{

BOOL isEqual = NO;

NSNumber *firstValueNumber = [NSNumber numberWithDouble:firstValue];
NSNumber *secondValueNumber = [NSNumber numberWithDouble:secondValue];

isEqual = [firstValueNumber isEqualToNumber:secondValueNumber];

return isEqual;

}

-1赞 denim 3/23/2020 #11

我正在使用以下比较函数来比较一些小数位:

bool compare(const double value1, const double value2, const int precision)
{
    int64_t magnitude = static_cast<int64_t>(std::pow(10, precision));
    int64_t intValue1 = static_cast<int64_t>(value1 * magnitude);
    int64_t intValue2 = static_cast<int64_t>(value2 * magnitude);
    return intValue1 == intValue2;
}

// Compare 9 decimal places:
if (compare(theView.frame.origin.x, 0, 9)) {
    // do important operation
}
0赞 dgnuff 9/2/2022 #12

另一个可能需要牢记的问题是,不同的实现以不同的方式做事。我非常熟悉的一个例子是索尼 Playstation 2 上的 FP 单元。与任何 X86 设备中的 IEEE FP 硬件相比,它们都存在显着差异。引用的文章提到完全缺乏对 inf 和 NaN 的支持,而且情况变得更糟。

鲜为人知的是,我所知道的“比特乘法”错误。对于某些值:float x

    y = x * 1.0;
    assert(y == x);

断言将失败。在一般情况下,有时(但并非总是)Playstation 2 上的 FP 乘法结果的尾数比等效的 IEEE 尾数少一位。

我的观点是,你不应该假设将FP代码从一个平台移植到另一个平台会产生相同的结果。任何给定的平台都是内部一致的,因为结果在该平台上不会改变,只是它们可能不同意不同的平台。例如,X86 上的 CPython 使用 64 位双精度值来表示浮点数,而 Cortex MO 上的 CircuitPython 必须使用软件 FP,并且仅使用 32 位浮点数。毋庸置疑,这将带来差异。

我 40 多年前学到的一句话在今天和我学到这句话的那一天一样真实。“在计算机上进行浮点数学运算就像移动一堆沙子。每次你做任何事情,你都会留下一点沙子,捡起一点泥土。

Playstation 是 Sony Corporation 的注册商标。