【问题标题】:Floating point equality浮点相等
【发布时间】:2018-12-10 13:18:35
【问题描述】:

众所周知,在比较浮点值时必须小心。通常,我们不使用 ==,而是使用一些基于 epsilon 或 ULP 的相等性测试。

但是,我想知道,是否有任何情况下,使用== 完全没问题?

看看这个简单的sn-p,哪些情况可以保证成功?

void fn(float a, float b) {
    float l1 = a/b;
    float l2 = a/b;

    if (l1==l1) { }        // case a)
    if (l1==l2) { }        // case b)
    if (l1==a/b) { }       // case c)
    if (l1==5.0f/3.0f) { } // case d)
}

int main() {
    fn(5.0f, 3.0f);
}

注意:我检查了 thisthis,但它们并没有涵盖我的(所有)案例。

注2:看来我必须添加一些附加信息,所以答案在实践中可能会有用:我想知道:

  • C++ 标准是怎么说的
  • 如果 C++ 实现遵循 IEEE-754 会发生什么

这是我在current draft standard中找到的唯一相关声明:

浮点类型的值表示是实现定义的。 [ 注:本文档对浮点运算的准确性没有要求;另见 [support.limits]。 — 尾注 ]

那么,这是否意味着,即使是“case a)”也是实现定义的?我的意思是,l1==l1 绝对是一个浮点运算。那么,如果一个实现是“不准确的”,那么l1==l1 可能是假的吗?


我认为这个问题不是Is floating-point == ever OK? 的重复问题。这个问题没有解决我要问的任何情况。相同的主题,不同的问题。我想有专门针对案例 a)-d) 的答案,但我无法在重复的问题中找到答案。

【问题讨论】:

  • @interjay:是的,我也是这么想的,但我不能 100% 确定。 IEEE 754 规定了这一点,但我不知道 C++ 标准对此有何规定,如果有的话。
  • 根本无法保证。
  • 您永远需要的答案:永远不要==
  • @iBug:你能解释一下原因吗?

标签: c++ floating-point language-lawyer precision floating-point-comparison


【解决方案1】:

但是,我想知道,是否有任何情况下,使用 == 完全没问题?

当然有。一类示例是不涉及计算的用法,例如只应在更改时执行的设置器:

void setRange(float min, float max)
{
    if(min == m_fMin && max == m_fMax)
        return;

    m_fMin = min;
    m_fMax = max;

    // Do something with min and/or max
    emit rangeChanged(min, max);
}

另请参阅 Is floating-point == ever OK?Is floating-point == ever OK?

【讨论】:

  • 不是那个。
  • @SombreroChicken:为什么不呢?
  • @SombreroChicken:有什么区别?
  • 它是否正确,它确实回答了这个问题“我想知道,有没有什么情况下,使用 == 完全没问题?
  • @luk32 x86 FP 堆栈的精度高于存储值。 x86 上的 ABI 允许在 FP 堆栈中传递浮点数。现在您选择精度大于内存浮点但仍可以存储在 FP 堆栈中的值。当您执行m_max = arg; 时,它将截断值。那么每个测试m_max == arg 都会失败,因为你在现实中检查trunc(arg) == arg
【解决方案2】:

人为的案例可能“有效”。实际案例可能仍然失败。另一个问题是,优化通常会导致计算方式的微小变化,因此结果在符号上应该相等,但在数值上它们是不同的。理论上,在这种情况下,上面的示例可能会失败。一些编译器提供了以性能为代价产生更一致结果的选项。我建议“始终”避免浮点数的相等性。

物理测量的相等性以及数字存储的浮点数通常是没有意义的。因此,如果您比较代码中的浮点数是否相等,则您可能做错了什么。您通常希望大于或小于该值或在容差范围内。通常可以重写代码,以避免这些类型的问题。

【讨论】:

  • 并非总是如此,请参阅 user2301274 的回答。等式可用于检查变量变化。例如,如果set 函数使用与当前值相同的值,它可以立即返回。
  • @geza 或者,更一般地说,浮点相等意味着“真实世界”的相等,但反过来不一定正确。
  • @geza:但是很容易设计出 set 函数不起作用的情况。例如。最初设置为 1.0,后来设置为实际为 1 的计算值。(足够的零用于精度)1。
  • 通常毫无意义”比保证有意义吗?
  • @curiousguy - 一个有意义的解决方案是真正满足您从客户简报中提炼或挖掘的真正正式要求的解决方案。除了计算机科学的调查之外,如果您正在编写 if(length_of_wood == 3.2),那么您的形式要求可能是错误的。简而言之,木头长 3.2m。对此进行编码是“毫无意义的”。正式的要求可能是,木头的测量长度应该在 3.19 到 3.21m 之间。有意义的是满足客户的需求。 (不要像要求一样对简报进行编码。)
【解决方案3】:

只有 a) 和 b) 可以保证在任何合理的实现中成功(有关详细信息,请参阅下面的法律术语),因为它们比较了以相同方式导出并四舍五入到 float 精度的两个值。因此,两个比较值都保证与最后一位相同。

情况 c) 和 d) 可能会失败,因为计算和后续比较可能会以比 float 更高的精度执行。 double 的不同舍入应该足以使测试失败。

请注意,如果涉及无穷大或 NAN,情况 a) 和 b) 仍可能失败。


法律术语

使用标准的 N3242 C++11 工作草案,我发现以下内容:

在描述赋值表达式的文本中,明确指出发生了类型转换,[expr.ass] 3:

如果左操作数不是类类型,则表达式被隐式转换(第 4 条)为左操作数的 cv 非限定类型。

第 4 条涉及标准转换 [conv],其中包含以下关于浮点转换的内容,[conv.double] 1:

浮点类型的纯右值可以转换为另一种浮点类型的纯右值。如果 源值可以精确地表示在目标类型中,转换的结果就是精确的 表示。 如果源值介于两个相邻的目标值之间,则转换结果 是这些值中任何一个的实现定义的选择。 否则,行为未定义。

(强调我的。)

所以我们保证转换的结果是实际定义的,除非我们处理的值超出了可表示的范围(比如float a = 1e300,即UB)。

当人们想到“内部浮点表示可能比代码中可见的更精确”时,他们会想到标准中的以下句子,[expr] 11:

浮动操作数的值和浮动表达式的结果可以用更大的形式表示 精度和范围超出类型要求;类型不会因此而改变。

请注意,这适用于操作数和结果,而不适用于变量。所附脚注 60 强调了这一点:

强制转换和赋值运算符仍必须按照 5.4、5.2.9 和 5.17 中的说明执行其特定转换。

(我猜,这是 Maciej Piechotka 在 cmets 中的意思——编号似乎在他一直使用的标准版本中发生了变化。)

所以,当我说float a = some_double_expression; 时,我保证表达式的结果实际上是四舍五入的,可以用float 表示(仅当值越界时才调用UB),并且@ 987654327@ 之后将引用该舍入值。

实现确实可以指定舍入的结果是随机的,从而打破 a) 和 b) 的情况。不过,理智的实现不会那样做。

【讨论】:

    【解决方案4】:

    假设 IEEE 754 语义,在某些情况下您肯定可以做到这一点。传统的浮点数计算在可能的情况下都是精确的,例如包括(但不限于)操作数和结果都是整数的所有基本运算。

    因此,如果您知道自己没有做任何会导致无法表示的事情的事实,那您就很好。例如

    float a = 1.0f;
    float b = 1.0f;
    float c = 2.0f;
    assert(a + b == c); // you can safely expect this to succeed
    

    只有当你的计算结果不能完全表示(或涉及不准确的操作)并且你改变了操作的顺序时,情况才会变得很糟糕。

    请注意,C++ 标准本身并不能保证 IEEE 754 语义,但大多数情况下您都可以预期到这一点。

    【讨论】:

    • @Ben 哎呀,是的,我已经多次编辑该示例。最终删除它,你是对的,即使我修复它也不会增加那么多。
    【解决方案5】:

    如果a == b == 0.0,情况 (a) 将失败。在这种情况下,运算产生 NaN,并且根据定义(IEEE,不是 C)NaN ≠ NaN。

    如果在线程执行过程中更改浮点舍入模式(或其他计算模式),情况 (b) 和 (c) 可能会在并行计算中失败。不幸的是,在实践中看到了这个。

    情况 (d) 可能不同,因为编译器(在某些机器上)可能会选择对 5.0f/3.0f 的计算进行常量折叠并将其替换为常量结果(未指定精度),而必须计算 a/b在目标机器上运行时(可能完全不同)。事实上,可以以任意精度执行中间计算。当以 80 位浮点执行中间计算时,我已经看到旧英特尔架构的差异,该语言甚至不直接支持这种格式。

    【讨论】:

    • "在这个线程的执行过程中改变了" 你的意思是被一个线程改变并影响到其他线程吗?
    • @curiousguy — 很遗憾,是的。具体来说,这是在 Intel 架构上,其中 D3D 库更改了 CPU 的浮点循环模式并且没有重置它。由于 FP 模式是 CPU 的属性,它会影响所有线程。现在,这种情况发生的可能性很低,但它已经发生在我身上。
    【解决方案6】:

    以我的拙见,您不应该依赖== 运算符,因为它有很多极端情况。最大的问题是舍入和扩展精度。在 x86 的情况下,浮点运算的精度可以比您存储在变量中的精度更高(如果您使用协处理器,IIRC SSE 运算使用与存储相同的精度)。

    这通常是好事,但这会导致以下问题: 1./2 != 1./2 因为一个值是表单变量,第二个来自浮点寄存器。在最简单的情况下,它会起作用,但如果您添加其他浮点操作,编译器可能会决定将一些变量拆分到堆栈中,更改它们的值,从而更改比较结果。

    要获得 100% 的确定性,您需要查看汇编并查看之前对这两个值进行了哪些操作。在不平凡的情况下,即使是顺序也可以改变结果。

    总的来说,使用== 有什么意义?您应该使用稳定的算法。这意味着即使值不相等,它们也可以工作,但它们仍然给出相同的结果。我知道== 唯一有用的地方是序列化/反序列化,你知道你想要什么结果,你可以改变序列化来归档你的目标。

    【讨论】:

      猜你喜欢
      • 2017-10-18
      • 2011-05-01
      • 1970-01-01
      • 1970-01-01
      • 2011-04-19
      • 2018-04-03
      • 1970-01-01
      • 2013-02-03
      相关资源
      最近更新 更多