由于浮点的“近似”性质,两组不同的值可能返回相同的值。
#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.0
和a - c = 0.0
,给定a,b
和a,c
与b != c
??
答案 0 :(得分:63)
IEEE-754标准是经过精心设计的,当且仅当两个值相等时,将两个值相减会产生零,除非从其自身中减去无穷大会产生NaN和/或异常。
不幸的是,C ++不需要符合IEEE-754,并且许多C ++实现都使用IEEE-754的某些功能,但并不完全符合。
一种常见的行为是将次标准结果“刷新”为零。这是硬件设计的一部分,可以避免正确处理次要结果的负担。如果此行为有效,则将两个非常小的但不同的数字相减可得出零。 (数字必须在正常范围的底部附近,并且在次正常范围内具有一些有效位。)
有时具有这种行为的系统可能会提供一种禁用它的方法。
要提防的另一行为是C ++不需要精确地执行浮点运算。它允许在中间运算和某些表达式的“收缩”中使用“超精度”。例如,a*b - c*d
可以通过使用一个将a
和b
相乘的运算,然后再将c
和d
相乘并从中减去结果的另一个运算来计算先前计算的a*b
。后面的操作就好像c*d
是用无限精度计算的,而不是四舍五入为标称浮点格式一样。在这种情况下,即使a*b - c*d
的计算结果为true,a*b == c*d
也会产生非零的结果。
某些C ++实现提供了禁用或限制此类行为的方法。
答案 1 :(得分:19)
渐进下溢功能可以防止这种情况。逐渐下溢是通过 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>
位置:
这在概念上类似于您在学校教过的科学计数法。
但是,这种格式具有许多相同的数字表示形式,几乎浪费了整个编码空间。要解决此问题,我们可以在尾数上添加一个“隐式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。