今天,我浏览了一些 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
吗?
numeric_limits<>::epsilon
的使用具有误导性且无关紧要。如果实际值与 0 的差异不超过某个 ε,我们想要假设 0。并且 ε 应该根据问题规范而不是与机器相关的值来选择。我怀疑当前的 epsilon 是无用的,因为即使是几个 FP 操作也可能累积比这更大的错误。
假设 64 位 IEEE double,则有 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%...
该测试肯定与someValue == 0
不同。浮点数的整个想法是它们存储一个指数和一个有效数。因此,它们表示具有一定数量的二进制有效数字精度的值(在 IEEE 双精度的情况下为 53)。可表示的值在 0 附近比在 1 附近更密集。
要使用更熟悉的十进制系统,假设您将十进制值存储为“4 位有效数字”和指数。那么下一个大于 1
的可表示值是 1.001 * 10^0
,而 epsilon
是 1.000 * 10^-3
。但是 1.000 * 10^-4
也是可表示的,假设指数可以存储 -4。你可以相信我的话,IEEE double 可以存储的指数小于 epsilon
的指数。
您无法仅从这段代码中判断将 epsilon
专门用作绑定是否有意义,您需要查看上下文。 epsilon
可能是对产生 someValue
的计算中的错误的合理估计,也可能不是。
someValue == 0.0
。
有些数字存在于 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;
}
X
与 X
的下一个值之间的差异因 X
而异。
epsilon()
只是 1
与 1
的下一个值之间的差异。
0
并且 0
的下一个值不是 epsilon()
。
相反,您可以使用 std::nextafter
将双精度值与 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;
}
nextafter
;但请注意,这种用法不太可能符合程序员的意图。假设 64 位 IEEE 754,在您的示例中 same(0, 1e-100)
返回 false,这可能不是程序员想要的。程序员可能宁愿想要一些小的阈值来测试相等性,例如 +/-1e-6
或 +/-1e-9
,而不是 +/-nextafter
。
假设我们正在使用适合 16 位寄存器的玩具浮点数。有一个符号位、一个 5 位指数和一个 10 位尾数。
这个浮点数的值是尾数,解释为二进制十进制值,乘以 2 的指数次方。
在 1 附近,指数等于 0。所以尾数的最小位数是 1024 的一部分。
接近 1/2 的指数是负一,所以尾数的最小部分是原来的一半。使用 5 位指数可以达到负 16,此时尾数的最小部分值 32m 中的一部分。在负 16 指数处,该值大约是 32k 的一部分,比我们上面计算的大约 1 的 epsilon 更接近于零!
现在这是一个玩具浮点模型,它不能反映真实浮点系统的所有怪癖,但反映小于 epsilon 的值的能力与真实浮点值相当相似。
由于尾数和指数部分,您不能将此应用于 0。由于指数,您可以存储比 epsilon 小的数字,但是当您尝试执行类似 (1.0 - "very small number") 之类的操作时,您将获得 1.0。 Epsilon 不是值的指标,而是值精度的指标,以尾数表示。它显示了我们可以存储多少个正确的后继十进制数字。
因此,假设系统无法区分 1.000000000000000000000 和 1.000000000000000000001。即 1.0 和 1.0 + 1e-20。你认为在 -1e-20 和 +1e-20 之间还有一些值可以表示吗?
epsilon
之间有很多浮点值。因为它是 floating 点,而不是定点。
对于 IEEE 浮点,在最小的非零正值和最小的非零负值之间,存在两个值:正零和负零。测试一个值是否在最小的非零值之间,相当于测试是否与零相等;然而,赋值可能会产生影响,因为它会将负零变为正零。
可以想象,浮点格式可能具有介于最小有限正负值之间的三个值:正无穷小、无符号零和负无穷小。我不熟悉实际上以这种方式工作的任何浮点格式,但这样的行为将是完全合理的,并且可以说比 IEEE 的更好(也许不够好到值得添加额外的硬件来支持它,但在数学上 1 /(1/INF)、1/(-1/INF) 和 1/(1-1) 应该代表三种不同的情况,说明三个不同的零)。我不知道是否有任何 C 标准会要求有符号的无穷小,如果它们存在,则必须比较等于零。如果他们不这样做,像上面这样的代码可以有效地确保例如将一个数字重复除以 2 最终会产生零,而不是停留在“无穷小”上。
“1 和大于 1 的最小值之间的差”表示 1 +“机器零”,大约为 10^-8 或 10^-16,具体取决于您是否分别使用双变量的浮点数。要查看机器零,您可以将 1 除以 2,直到计算机看到 1 = 1+1/2^p,如下所示:
#include <iostream>
#include "math.h"
using namespace std;
int main() {
float a = 1;
int n = 0;
while(1+a != 1){
a = a/2;
n +=1;
}
cout << n-1 << endl << pow(2,-n);
return 0;
}
此外,拥有这样一个函数的一个很好的理由是删除“非规范化”(那些不能再使用隐含的前导“1”并具有特殊 FP 表示的非常小的数字)。你为什么想做这个?因为有些机器(特别是一些较旧的 Pentium 4s)在处理非规范化时会变得非常非常慢。其他人只是变得有点慢。如果您的应用程序并不真正需要这些非常小的数字,那么将它们清零是一个很好的解决方案。考虑这一点的好地方是任何 IIR 滤波器或衰减函数的最后一步。
另请参阅:Why does changing 0.1f to 0 slow down performance by 10x?
和http://en.wikipedia.org/wiki/Denormal_number
不定期副业成功案例分享
0 <= e < 2048
,则尾数乘以 2 的e - 1023
次方。例如,2^0
的指数编码为e=1023
,2^1
编码为e=1024
,2^-1022
编码为e=1
。e=0
的值是为次正规和实零保留的。2^-1022
也是最小的 正常 数。最小的数字实际上是0.0000 00000000 00000000 00000000 00000000 00000000 00000001 × 2^-1022 = 2^-1074
。这是次正规的,意味着尾数部分小于 1,所以它用指数e=0
编码。