在浮点数中是否有可能返回0.0减去两个不同的值?

时间:2019-02-05 09:41:03

标签: c++ floating-point

由于浮点的“近似”性质,两组不同的值可能返回相同的值。

Example

#include <iostream>

int main() {
    std::cout.precision(100);

    double a = 0.5;
    double b = 0.5;
    double c = 0.49999999999999994;

    std::cout << a + b << std::endl; // output "exact" 1.0
    std::cout << a + c << std::endl; // output "exact" 1.0
}

但是减法也可能吗?我的意思是:是否有两组不同的值(保留其中一个值)返回0.0

a - b = 0.0a - c = 0.0,给定a,ba,cb != c ??

4 个答案:

答案 0 :(得分:63)

IEEE-754标准是经过精心设计的,当且仅当两个值相等时,将两个值相减会产生零,除非从其自身中减去无穷大会产生NaN和/或异常。

不幸的是,C ++不需要符合IEEE-754,并且许多C ++实现都使用IEEE-754的某些功能,但并不完全符合。

一种常见的行为是将次标准结果“刷新”为零。这是硬件设计的一部分,可以避免正确处理次要结果的负担。如果此行为有效,则将两个非常小的但不同的数字相减可得出零。 (数字必须在正常范围的底部附近,并且在次正常范围内具有一些有效位。)

有时具有这种行为的系统可能会提供一种禁用它的方法。

要提防的另一行为是C ++不需要精确地执行浮点运算。它允许在中间运算和某些表达式的“收缩”中使用“超精度”。例如,a*b - c*d可以通过使用一个将ab相乘的运算,然后再将cd相乘并从中减去结果的另一个运算来计算先前计算的a*b。后面的操作就好像c*d是用无限精度计算的,而不是四舍五入为标称浮点格式一样。在这种情况下,即使a*b - c*d的计算结果为true,a*b == c*d也会产生非零的结果。

某些C ++实现提供了禁用或限制此类行为的方法。

答案 1 :(得分:19)

IEEE浮点标准的

渐进下溢功能可以防止这种情况。逐渐下溢是通过 subnormal denormal )数实现的,该数均匀分布(与对数类似,与正常浮点相反),并且位于最小的负正数与正数之间中间有零。由于它们之间的间隔均匀,因此两个不同符号的次正规数(即减为零)的加法是精确的,因此不会重现您的要求。最小的法线小于(远)于法线数之间的最小距离,因此,不相等的法线数之间的任何减法都将接近于零以下的法线。

如果使用CPU的特殊 denormals-are-zero(DAZ) flush-to-zero(FTZ)模式禁用IEEE一致性,那么实际上可能会减去两个小而接近的数字,否则将导致不正常的数字,由于CPU的模式,该数字将被视为零。 working example(Linux):

_MM_SET_FLUSH_ZERO_MODE(_MM_FLUSH_ZERO_ON);    // system specific
double d = std::numeric_limits<double>::min(); // smallest normal
double n = std::nextafter(d, 10.0);     // second smallest normal
double z = d - n;       // a negative subnormal (flushed to zero)
std::cout << (z == 0) << '\n' << (d == n);

这应该打印

1
0

第一个1表示相减的结果正好为零,而第二个0表示操作数不相等。

答案 2 :(得分:7)

不幸的是,答案取决于您的实现及其配置方式。 C和C ++不需要任何特定的浮点表示或行为。大多数实现使用IEEE 754表示形式,但它们并不总是精确实现IEEE 754算术行为。

要了解该问题的答案,我们首先必须了解浮点数的工作原理。

幼稚的浮点表示形式将具有一个指数,一个符号和一个尾数。值会是

(-1) s 2 (e – e 0 (m / 2 M )< / p>

位置:

  • s是符号位,值为0或1。
  • e是指数字段
  • e 0 是指数偏差。它实际上是设置浮点数的整体范围。
  • M是尾数位数。
  • m是0到2 M -1
  • 之间的尾数

这在概念上类似于您在学校教过的科学计数法。

但是,这种格式具有许多相同的数字表示形式,几乎浪费了整个编码空间。要解决此问题,我们可以在尾数上添加一个“隐式1”。

(-1) s 2 (e – e 0 (1+(m / 2 M ))

此格式仅对每个数字表示一个。但是存在一个问题,它不能表示零或接近零的数字。

要解决此问题,IEEE浮点数在特殊情况下会保留几个指数值。指数值零保留用于表示小的数字,这些子数字称为次法线。最高的指数值保留给NaN和无穷大(在本文中我将忽略,因为它们与此处无关)。这样定义就变成了。

(-1) s 2 (1 – e 0 (m / 2 M )当e = 0
(-1) s 2 (e – e 0 (1+(m / 2 M )))当e> 0和e <2 E -1

使用此表示,较小的数字始终具有小于或等于较大数字的步长。因此,如果相减的结果在大小上小于两个操作数,则可以精确表示。特别是接近但不完全为零的结果可以准确表示。

如果结果的大小大于一个或两个操作数,例如从大值中减去一个小值或两个相反符号的值相减,则此方法不适用。在这些情况下,结果可能不精确,但显然不能为零。

不幸的是,FPU设计师偷工减料。他们不是完全不支持(非零)次态,而是为次态提供了缓慢的支持,然后为用户提供了将其打开和关闭的选项,而不是包括快速正确地处理次态数的逻辑。如果不存在或禁用对正确的次正规计算的支持,并且该数字太小而无法以规范化形式表示,则它将“降至零”。

因此,在现实世界中,在某些系统和配置下,减去两个不同的非常小的浮点数可能会导致答案为零。

答案 3 :(得分:3)

排除像NAN这样的有趣数字,我认为这是不可能的。

假设a和b是普通的有限IEEE 754浮点数,| a-b |小于或等于两个| a |和| b | (否则显然不是零)。

这意味着指数是<= a和b的总和,因此绝对精度至少是一样高的,这使得减法可以精确表示。这意味着如果a-b == 0,则它恰好为零,因此a == b。

相关问题