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

mar*_*zzz 46 c++ floating-point

由于浮点"近似"性质,两个不同的值集可能返回相同的值.

示例:

#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
}
Run Code Online (Sandbox Code Playgroud)

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

a - b = 0.0a - c = 0.0,给出了一些套a,ba,cb != c

Eri*_*hil 64

IEEE-754标准是故意设计的,当且仅当两个值相等时,减去两个值才产生零,除了从自身减去无穷大产生NaN和/或异常.

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

一种不常见的行为是将次正常结果"刷新"为零.这是硬件设计的一部分,以避免正确处理次正常结果的负担.如果此行为有效,则减去两个非常小但不同的数字可以产生零.(数字必须接近正常范围的底部,在次正常范围内有一些有效位.)

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

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

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


eer*_*ika 19

IEEE浮点标准的逐渐下溢功能可以防止这种情况发生.逐渐下溢是通过次正规(非正规)数来实现的,这些数均匀间隔(与对数相反,与正常浮点相反),位于最小的正和正正数之间,中间为零.因为它们是均匀间隔的,所以添加两个不同符号的次正规数(即减零)是精确的,因此不会重现你所要求的.最小的次正规(正常)小于正常数之间的最小距离,因此不等正常数之间的任何减法将更接近于低于零的正常.

如果使用CPU 的特殊denormals-zero-zero(DAZ)flush-to-zero(FTZ)模式禁用IEEE一致性,那么实际上你可以减去两个小的,接近的数字,否则会导致一个次正规数,由于CPU的模式,将被视为零.一个工作示例(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);
Run Code Online (Sandbox Code Playgroud)

这应该打印

1
0
Run Code Online (Sandbox Code Playgroud)

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


plu*_*ash 7

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

要理解这个问题的答案,首先要了解浮点数是如何工作的.

一个朴素的浮点表示将有一个指数,一个符号和一个尾数.它的价值将是

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

哪里:

  • 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)当e> 0和e 时e = 0 (-1)s 2 (e - e 0)(1+(m/2 M))时s 2 (1 - e 0)(m/2 M)<2 E -1

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

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

不幸的是FPU设计师偷工减料.它们不是快速正确地包含处理次正规数的逻辑,而是根本不支持(非零)次正规或者提供对次正规的慢速支持,然后让用户选择打开和关闭它.如果不存在或禁用对正确的次正规计算的支持且数量太小而无法以标准化形式表示,那么它将"刷新为零".

因此,在现实世界中,在一些系统和配置下,减去两个不同的非常小的浮点数可以得到零答案.

  • 问题的利息编号是减法的“结果”是次正规的。将两个较小的正常数相减会产生一个低于正常的结果,某些实现会将其冲刷为零。 (2认同)
  • 英特尔处理器上的“ FTZ”和“ DAZ”模式之间的差异似乎是发生刷新的地方。前者对操作的输出进行操作,而后者对输入进行操作。因此,如果我使用FTZ正确阅读文档,则减法会产生假零,而使用DAZ时,减法会产生正确的反常结果,但是比较之后会将反常结果视为零。 (2认同)

Jos*_*and 3

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

假设 a 和 b 是正常的有限 IEEE 754 浮点数,并且 |a - b| 小于或等于|a| 和|b| (否则它显然不为零)。

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