46

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

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

看看这个简单的代码片段,哪些情况可以保证成功?

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 会发生什么

这是我在当前标准草案中找到的唯一相关声明:

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

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


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

4

6 回答 6

15

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

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

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);
}

另请参阅浮点 == 是否可以?并且浮点 == 可以吗?.

于 2018-07-02T11:12:19.463 回答
6

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

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

于 2018-07-02T11:27:56.937 回答
4

只有 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 = 1e300UB)。

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

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

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

强制转换和赋值运算符仍必须执行其特定的转换,如 5.4、5.2.9 和 5.17 中所述。

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

因此,当我说 时float a = some_double_expression;,我保证表达式的结果实际上是四舍五入的,可以用 a 表示float(仅当值越界时才调用 UB),然后a会引用该舍入值。

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

于 2018-07-02T11:23:43.880 回答
2

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

当浮点循环模式(或其他计算模式)在线程执行过程中发生变化时,情况 (b) 和 (c) 可能会在并行计算中失败。不幸的是,在实践中看到了这个。

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

于 2018-07-04T01:46:16.897 回答
2

假设 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 语义,但大多数情况下您可以期望处理这些。

于 2018-07-02T15:06:20.407 回答
0

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

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

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

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

于 2018-07-02T11:44:06.497 回答