今天,我正在查看一些C ++代码(由其他人编写)并找到了这一部分:
double someValue = ...
if (someValue < std::numeric_limits<double>::epsilon() &&
someValue > -std::numeric_limits<double>::epsilon()) {
someValue = 0.0;
}
我试图弄清楚这是否有意义。
epsilon()
的文档说:
该函数返回1和大于1的最小值之间的差值,可以表示[乘以]。
这是否也适用于0,即epsilon()
是大于0的最小值?或者0
和0 + epsilon
之间是否有数字可以用double
表示?
如果没有,那么是不是与someValue == 0.0
相当的比较?
假设64位IEEE双,则有52位尾数和11位指数。让我们把它分解成位:
1.0000 00000000 00000000 00000000 00000000 00000000 00000000 × 2^0 = 1
可表示的最小数字大于1:
1.0000 00000000 00000000 00000000 00000000 00000000 00000001 × 2^0 = 1 + 2^-52
因此:
epsilon = (1 + 2^-52) - 1 = 2^-52
0和epsilon之间有数字吗?很多...例如最小正可表示(正常)数字是:
1.0000 00000000 00000000 00000000 00000000 00000000 00000000 × 2^-1022 = 2^-1022
事实上,在0和epsilon之间有(1022 - 52 + 1)×2^52 = 4372995238176751616
数字,这是所有可表示的数字的47%......
对于IEEE浮点,在最小的非零正值和最小的非零负值之间,存在两个值:正零和负零。测试值是否在最小的非零值之间等同于测试等于零的值;但是,分配可能会产生影响,因为它会将负零变为正零。
可以想象浮点格式在最小有限正值和负值之间可能具有三个值:正无穷小,无符号零和负无穷小。我不熟悉实际上以这种方式工作的任何浮点格式,但这种行为完全合理且可以说比IEEE更好(可能不足以值得添加额外的硬件来支持它,但在数学上1 /(1 / INF),1 /( - 1 / INF)和1 /(1-1)应代表三个不同的情况,说明三个不同的零)。我不知道任何C标准是否会强制签署的无穷小,如果它们存在,则必须比较等于零。如果他们不这样做,那么像上面这样的代码可以有效地确保例如将一个数字重复除以2最终会产生零而不是被卡在“无穷小”上。
此外,具有这种功能的一个很好的理由是删除“非正规”(那些非常小的数字,不能再使用隐含的前导“1”并具有特殊的FP表示)。你为什么想做这个?因为有些机器(特别是一些较旧的Pentium 4)在处理非正规数时会变得非常非常慢。其他人只是变慢了一些。如果您的应用程序确实不需要这些非常小的数字,则将它们刷新为零是一个很好的解决方案。考虑这一点的好地方是任何IIR滤波器或衰减功能的最后步骤。
另见:Why does changing 0.1f to 0 slow down performance by 10x?
测试肯定与someValue == 0
不同。浮点数的整个概念是它们存储指数和有效数。因此,它们表示具有一定数量的二进制有效精度数值的值(在IEEE双精度的情况下为53)。可表示的值在0附近比在1附近密集得多。
要使用更熟悉的十进制系统,假设您使用指数存储十进制值“到4个有效数字”。那么大于1
的下一个可表示的值是1.001 * 10^0
,而epsilon
是1.000 * 10^-3
。但是1.000 * 10^-4
也可以表示,假设指数可以存储-4。您可以接受我的说法,IEEE双倍可以存储小于epsilon
指数的指数。
你无法从这个代码中单独判断是否有意义地使用epsilon
作为绑定,你需要查看上下文。可能是epsilon
是计算中产生someValue
的误差的合理估计,并且它可能不是。
在0和epsilon之间存在数字,因为epsilon是1和可以在1之上表示的下一个最高数字之间的差异,而不是0和可以在0之上表示的下一个最高数字之间的差异(如果是,那个代码会做的很少): -
#include <limits>
int main ()
{
struct Doubles
{
double one;
double epsilon;
double half_epsilon;
} values;
values.one = 1.0;
values.epsilon = std::numeric_limits<double>::epsilon();
values.half_epsilon = values.epsilon / 2.0;
}
使用调试器,在main结束时停止程序并查看结果,您将看到epsilon / 2与epsilon,0和1不同。
因此,此函数采用+/- epsilon之间的值并使它们为零。
可以使用以下程序打印数字(1.0,0.0,...)附近的epsilon(最小可能差异)的近似值。它打印以下输出:
epsilon for 0.0 is 4.940656e-324
epsilon for 1.0 is 2.220446e-16
一点思考清楚地表明,epsilon越小,我们用来查看其epsilon值的数字越小,因为指数可以调整到该数字的大小。
#include <stdio.h>
#include <assert.h>
double getEps (double m) {
double approx=1.0;
double lastApprox=0.0;
while (m+approx!=m) {
lastApprox=approx;
approx/=2.0;
}
assert (lastApprox!=0);
return lastApprox;
}
int main () {
printf ("epsilon for 0.0 is %e\n", getEps (0.0));
printf ("epsilon for 1.0 is %e\n", getEps (1.0));
return 0;
}
假设我们正在使用适合16位寄存器的玩具浮点数。有一个符号位,一个5位指数和一个10位尾数。
此浮点数的值是尾数,被解释为二进制十进制值,是指数幂的两倍。
大约1指数等于零。所以尾数的最小数字是1024的一部分。
指数的1/2接近-1,因此尾数的最小部分是一半。使用五位指数,它可以达到负16,此时尾数的最小部分值得在32米中的一个部分。在负16指数处,该值约为32k的一部分,比我们上面计算的epsilon更接近零!
现在这是一个玩具浮点模型,它不能反映真实浮点系统的所有怪癖,但反映小于epsilon的值的能力与实际浮点值相当。
X
和X
的下一个值之间的差异根据X
而有所不同。
epsilon()
只是1
和1
的下一个值之间的差异。
0
和0
的下一个值之间的差异不是epsilon()
。
相反,您可以使用std::nextafter
将double值与0
进行比较,如下所示:
bool same(double a, double b)
{
return std::nextafter(a, std::numeric_limits<double>::lowest()) <= b
&& std::nextafter(a, std::numeric_limits<double>::max()) >= b;
}
double someValue = ...
if (same (someValue, 0.0)) {
someValue = 0.0;
}
由于尾数和指数部分,您不能将此应用于0。由于指数,你可以存储非常少的数字,这些数字小于epsilon,但是当你尝试做类似的事情(1.0 - “非常小的数字”)时你会得到1.0。 Epsilon是一个不是有价值的指标,而是价值精度的指标,它是尾数。它显示了我们可以存储多少个正确的数字后续十进制数字。
因此,假设系统无法区分1.000000000000000000000和1.000000000000000000001。这是1.0和1.0 + 1e-20。你认为仍然有一些值可以在-1e-20和+ 1e-20之间表示吗?