浮点相等



众所周知,在比较浮点值时必须小心。通常,我们不使用==,而是使用一些基于ε或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);
}

注意:我已经检查了这个和这个,但它们不包括(所有)我的病例。

注2:似乎我必须添加一些额外的信息,所以答案在实践中可能很有用:我想知道:

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

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

浮点类型的值表示是由实现定义的。[注:本文档对浮点运算的精度没有任何要求;另请参阅[support.limits]--尾注]

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


我认为这个问题不是浮点==是否可以?的重复?。这个问题没有解决我所问的任何一个问题。相同的主题,不同的问题。我想知道具体情况a)-d)的答案,因为我在重复的问题中找不到答案。

然而,我想知道,有没有任何情况下,使用==是完全可以的?

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

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

另请参阅浮点==是否正常浮点==是否正常

有争议的案例可能会"奏效"。实际案例可能仍然会失败。一个额外的问题是,优化通常会导致计算方式的微小变化,因此符号上的结果应该相等,但数字上的结果不同。从理论上讲,在这种情况下,上面的例子可能会失败。一些编译器提供了一种以牺牲性能为代价生成更一致结果的选项。我建议"永远"避免浮点数相等。

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

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

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

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


法律

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

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

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

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

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

(强调矿。)

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

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

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

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

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

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

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

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

假设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语义,但这正是您在大多数情况下需要处理的问题。

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

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

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

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

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

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

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

相关内容

  • 没有找到相关文章

最新更新