ChatGPT解决这个技术问题 Extra ChatGPT

比较浮点值有多危险?

由于与分辨率无关的坐标系,我知道 UIKit 使用 CGFloat

但是每次我想检查例如 frame.origin.x 是否为 0 时,都会让我感到恶心:

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

==<=>=<> 相比,CGFloat 是否容易出现误报?它是一个浮点数,它们有不精确的问题:例如 0.0000000000041

比较时 Objective-C 是否在内部处理这个问题,或者读为零的 origin.x0 比较是否为真?

这主要是非整数值的问题,其中很容易发生舍入错误。编写了一个 blog post,描述了何时发生舍入错误以及如何估计潜在错误的大小。

Z
Ziezi

首先,浮点值的行为不是“随机的”。精确比较在大量实际应用中可以而且确实是有意义的。但是如果你要使用浮点,你需要知道它是如何工作的。错误地假设浮点数像实数一样工作会让你的代码快速中断。错误地假设浮点结果具有与之相关的大量随机模糊(就像这里的大多数答案所建议的那样)将使您的代码起初看起来可以工作,但最终会出现大量错误和破坏的极端情况。

首先,如果你想用浮点编程,你应该阅读这个:

What Every Computer Scientist Should Know About Floating-Point Arithmetic

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

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

事实上,您可以在源代码中写入或使用 scanf 或 strtod 读取的许多值不作为浮点值存在,而是默默地转换为最接近的近似值。这就是demon9733的回答所说的。由于没有足够的精度来表示实际结果,许多结果会被四舍五入。一个简单的例子是添加 x = 0x1fffffe 和 y = 1 作为浮点数。在这里,x 在尾数(ok)中具有 24 位精度,而 y 只有 1 位,但是当您将它们相加时,它们的位不在重叠的位置,结果需要 25 位精度。相反,它会被舍入(在默认舍入模式下为 0x2000000)。事实上,由于需要无数个位置来获得正确的值,许多结果都会被四舍五入。这包括像 1/3 这样的有理结果(你从十进制中熟悉它,它占据了无限多的位置),也包括 1/10(它在二进制中也占据了无限多的位置,因为 5 不是 2 的幂),以及不合理的结果,例如任何不是完美平方的平方根。双圆。在某些系统(尤其是 x86)上,浮点表达式的计算精度高于其标称类型。这意味着当发生上述其中一种舍入时,您将获得两个舍入步骤,首先将结果舍入为更高精度类型,然后舍入为最终类型。例如,考虑将 1.49 四舍五入为整数 (1) 时会发生什么,与先将其四舍五入到小数点后一位 (1.5) 然后将该结果四舍五入为整数 (2) 会发生什么情况。这实际上是浮点处理中最讨厌的领域之一,因为编译器的行为(尤其是对于像 GCC 这样的有缺陷、不符合标准的编译器)的行为是不可预测的。超越函数(trig、exp、log 等)未指定具有正确舍入的结果;结果只是在精度的最后一位(通常称为 1ulp)的一个单位内指定为正确的。

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

编辑:特别是,相对于幅度的 epsilon 检查应该类似于:

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

其中 FLT_EPSILON 是来自 float.h 的常数(用 DBL_EPSILON 代替 doubleLDBL_EPSILON 代替 long double),K 是您选择的常数,以便计算的累积误差在最后一个位置肯定是由 K 个单位限制的(如果您不确定您的误差范围计算是否正确,请使 K 比您的计算结果大几倍)。

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

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

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


如果 xy(可以)具有不同的符号,则 fabs(x+y) 有问题。尽管如此,对于货物狂热的比较潮流来说,这是一个很好的答案。
如果 xy 有不同的符号,那没问题。右边会“太小”,但由于 xy 有不同的符号,所以它们不应该比较相等。 (除非它们太小以至于不正常,但是第二种情况会抓住它)
我很好奇你的说法:“特别是对于像 GCC 这样的错误、不符合标准的编译器”。真的是 GCC 错误且不符合标准吗?
由于该问题被标记为 iOS,因此值得注意的是 Apple 的编译器(clang 和 Apple 的 gcc 构建)一直使用 FLT_EVAL_METHOD = 0,并试图完全严格地不携带过多的精度。如果您发现任何违规行为,请提交错误报告。
“首先,浮点值的行为不是“随机”的。精确比较在许多现实世界的使用中是有意义的。” - 只需两句话,就已经获得了+1!这是人们在处理浮点数时最令人不安的错误假设之一。
H
High Performance Mark

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

为了澄清一点,计算如下:

areal = 0.0

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

areal = 1.386 - 2.1*(0.66)

不得。

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

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

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


实际上,1.30 - 2*(0.65) 是一个完美的表达式示例,如果您的编译器实现 IEEE 754,则显然计算结果为 0.0,因为表示为 0.651.30 的双精度数具有相同的有效位,并且乘以 2 显然是精确的。
仍然从这个获得代表,所以我更改了第二个示例片段。
s
starmole

我想给出一些与其他人不同的答案。它们非常适合回答您所说的问题,但可能不适用于您需要了解的内容或您的真正问题。

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

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

但是,如果您将图形坐标用作某种标志,例如查看窗口是否停靠,则不应这样做。改为使用与图形表示层分离的布尔标志。


J
JHumphrey

与零比较可以是一种安全操作,只要零不是计算值(如上述答案中所述)。这样做的原因是零是一个完美的浮点数。

谈到完全可表示的值,您将获得 24 位范围的二次幂概念(单精度)。所以 1、2、4 是完全可表示的,0.5、0.25 和 0.125 也是如此。只要你所有重要的位都是 24 位的,你就是黄金。所以 10.625 可以精确地表示。

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

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


G
GoZoner

[“正确答案”掩盖了选择 K。选择 K 最终与选择 VISIBLE_SHIFT 一样临时,但选择 K 不太明显,因为与 VISIBLE_SHIFT 不同,它不基于任何显示属性。因此选择你的毒药 - 选择 K 或选择 VISIBLE_SHIFT。这个答案主张选择VISIBLE_SHIFT,然后证明选择K的难度]

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

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

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

现在,“正确答案”取决于 K,让我们来探索选择 K。上面的“正确答案”说:

K 是您选择的常数,这样您的计算的累积误差肯定会在最后一个位置受到 K 个单位的限制(如果您不确定您的误差限制计算是否正确,请使 K 比您的计算大几倍说应该是)

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

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

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 或更大。

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

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


分辨率独立性的一个方面是您无法确定编译时的“可见偏移”是什么。在超高清屏幕上看不见的东西很可能在小屏幕上很明显。至少应该让它成为屏幕尺寸的函数。或将其命名为其他名称。
但至少选择“可见移位”是基于易于理解的显示(或框架)属性 - 不像<正确答案> K 难以选择且不直观。
M
Michael T

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

正确答案:CGPointEqualToPoint()。

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

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

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


L
Lucas Membrane

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

也就是说,尽管我每次看到它都努力删除它,但我工作的机构有很多使用 gcc 编译并在 linux 上运行的 C 代码,而且我们很长一段时间都没有注意到这些意外结果.我不知道这是否是因为 gcc 正在为我们清除低位,现代计算机上的这些操作没有使用 80 位寄存器,标准已经改变,或者什么。我想知道是否有人可以引用章节和经文。


I
Igor

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

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

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


在未确保 theView.frame.origin.xint 范围内/附近的情况下强制转换为 int 会导致未定义行为 (UB) - 或者在本例中为 int 范围的 1/100。
绝对没有理由像这样转换为整数。正如chux所说,UB有可能来自超出范围的值;在某些架构上,这将比仅在浮点中进行计算要慢得多。最后,像这样乘以 100 将与 0.01 精度进行比较,而不是 0.1。
A
Abbas Mulani
-(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;

}


d
denim

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

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
}

J
John White

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

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


这不是一个好的答案。首先,整个“对象”对解决您的问题没有任何作用。其次,您对“平等”的实际实施实际上并不正确。
汤姆,也许你会再想想“对象的东西”。对于以高精度表示的实数,很少发生相等。但是,如果适合您,一个人的平等观念可能会量身定制。如果有一个可覆盖的“近似相等”运算符会更好,但没有。