众所周知,比较浮点值时必须小心。通常,我们使用一些基于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);
}
注意:我已经检查了this和this,但它们并不能涵盖我的全部情况。
注2:似乎我必须添加一些附加信息,因此答案在实践中可能是有用的:我想知道:
这是我在current draft standard中找到的唯一相关声明:
浮点类型的值表示形式是实现定义的。 [注:本文档对浮点运算的准确性没有任何要求;另请参见[support.limits]。 —注释]
那么,这是否意味着甚至定义了“案例a)”?我的意思是,l1==l1
绝对是浮点运算。因此,如果实现是“不准确的”,那么l1==l1
会为假吗?
我认为这个问题不是Is floating-point == ever OK?的重复。这个问题没有解决我要问的任何情况。同一主题,不同问题。我想针对情况a)-d)专门找到答案,为此我无法在重复的问题中找到答案。
答案 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%的确定性,您需要查看汇编并查看在这两个值上进行过哪些操作。在非平凡的情况下,即使订单也可以更改结果。
总体而言,使用==
有什么意义?您应该使用稳定的算法。这意味着即使值不相等,它们也可以工作,但是它们仍然给出相同的结果。我唯一知道==
有用的地方是序列化/反序列化,在这里您确切地知道想要什么结果,并且可以更改序列化以归档目标。