浮点数相等

时间:2018-07-02 10:26:35

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

众所周知,比较浮点值时必须小心。通常,我们使用一些基于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,会发生什么情况

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

  

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

那么,这是否意味着甚至定义了“案例a)”?我的意思是,l1==l1绝对是浮点运算。因此,如果实现是“不准确的”,那么l1==l1会为假吗?


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

6 个答案:

答案 0 :(得分: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);
}

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

答案 1 :(得分:6)

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

物理测量的平等以及数字存储的浮子通常是毫无意义的。因此,如果比较代码中的浮点数是否相等,则可能是您做错了什么。您通常希望大于或小于该值或在一个公差范围内。通常可以重写代码,从而避免出现此类问题。

答案 2 :(得分:4)

只有a)和b)可以保证在任何理智的实现中都能成功(有关详细信息,请参见下面的法文),因为它们比较以相同方式得出并精确到float的两个值。因此,两个比较值都保证与最后一位相同。

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

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


法文

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

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

  

如果左操作数不是类类型,则将表达式隐式转换(第4条)为左操作数的cv不合格类型。

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

  

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

(强调我的。)

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

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

  

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

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

  

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

(我想这是Maciej Piechotka在注解中的脚注-在他所使用的标准版本中,编号似乎已经更改。)

因此,当我说float a = some_double_expression;时,我保证表达式的结果实际上被四舍五入为float所表示的(仅当值超出边界时才调用UB) ),然后a将引用该舍入值。

一个实现确实可以指定舍入的结果是随机的,从而破坏了情况a)和b)。但是,Sane的实现无法做到这一点。

答案 3 :(得分:1)

假设采用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语义,但这就是大多数时候您可以期望的。

答案 4 :(得分:1)

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

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

情况(d)可能有所不同,因为编译器(在某些计算机上)可能选择对5.0f/3.0f的计算进行常量折叠,并用常量结果(精度未指定)替换,而{{1} }必须在目标机器上的运行时进行计算(可能会大不相同)。实际上,中间计算可以任意精度执行。当以80位浮点数执行中间计算时,我已经看到了旧的Intel体系结构的差异,该语言甚至不直接支持这种格式。

答案 5 :(得分:0)

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

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

要具有100%的确定性,您需要查看汇编并查看在这两个值上进行过哪些操作。在非平凡的情况下,即使订单也可以更改结果。

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