ChatGPT解决这个技术问题 Extra ChatGPT

使用 epsilon 将双精度数与零进行比较

今天,我浏览了一些 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 的最小值?或者在 00 + epsilon 之间是否有可以用 double 表示的数字?

如果不是,那么比较不等于 someValue == 0.0 吗?

1 附近的 epsilon 很可能远高于 0 附近的 epsilon,因此可能会有 0 到 0+epsilon_at_1 之间的值。我猜这部分的作者想使用一些小的东西,但他不想使用魔法常数,所以他只是使用了这个本质上是任意值。
比较浮点数很困难,甚至鼓励使用 epsilon 或阈值。请参考:cs.princeton.edu/introcs/91floatcygnus-software.com/papers/comparingfloats/comparingfloats.htm
第一个链接是 403.99999999
epsilon() 是最小的正值。因此,如果我们假设 epsilon() 是 e,我们得到 1+e != 1,所以是的,epsilon 是大于 0 的最小值,并且在 0 和 0 + e 之间没有数字
IMO,在这种情况下,numeric_limits<>::epsilon 的使用具有误导性且无关紧要。如果实际值与 0 的差异不超过某个 ε,我们想要假设 0。并且 ε 应该根据问题规范而不是与机器相关的值来选择。我怀疑当前的 epsilon 是无用的,因为即使是几个 FP 操作也可能累积比这更大的错误。

Y
Yakov Galka

假设 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%...


太奇怪了,你可以说“47% 的正数”:)
@configurator:不,你不能这么说(不存在“自然”有限度量)。但是您可以说“47% 的正可表示数字”。
@ybungalobill 我想不通。指数有 11 位:1 个符号位和 10 个值位。为什么 2^-1022 而不是 2^-1024 是最小的正数?
@PavloDyban:仅仅因为指数 没有 有符号位。它们被编码为偏移量:如果编码指数为 0 <= e < 2048,则尾数乘以 2 的 e - 1023 次方。例如,2^0 的指数编码为 e=10232^1 编码为 e=10242^-1022 编码为 e=1e=0 的值是为次正规和实零保留的。
@PavloDyban:2^-1022 也是最小的 正常 数。最小的数字实际上是 0.0000 00000000 00000000 00000000 00000000 00000000 00000001 × 2^-1022 = 2^-1074。这是次正规的,意味着尾数部分小于 1,所以它用指数 e=0 编码。
S
Steve Jessop

该测试肯定与someValue == 0不同。浮点数的整个想法是它们存储一个指数和一个有效数。因此,它们表示具有一定数量的二进制有效数字精度的值(在 IEEE 双精度的情况下为 53)。可表示的值在 0 附近比在 1 附近更密集。

要使用更熟悉的十进制系统,假设您将十进制值存储为“4 位有效数字”和指数。那么下一个大于 1 的可表示值是 1.001 * 10^0,而 epsilon1.000 * 10^-3。但是 1.000 * 10^-4 也是可表示的,假设指数可以存储 -4。你可以相信我的话,IEEE double 可以存储的指数小于 epsilon 的指数。

您无法仅从这段代码中判断将 epsilon 专门用作绑定是否有意义,您需要查看上下文。 epsilon 可能是对产生 someValue 的计算中的错误的合理估计,也可能不是。


好点,但即使是这种情况,更好的做法是将错误限制在一个合理命名的变量中并在比较中使用它。就目前而言,它与魔法常数没有什么不同。
也许我的问题应该更清楚:我没有质疑 epsilon 是否是一个足够大的“阈值”来涵盖计算错误,而是这个比较是否等于 someValue == 0.0
S
Skizz

有些数字存在于 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 之间的值并将它们设为零。


p
pbhd

可以使用以下程序打印数字 (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;
}

您检查了哪些实现? GCC 4.7 绝对不是这种情况。
D
Daniel Laügt

XX 的下一个值之间的差异因 X 而异。
epsilon() 只是 11 的下一个值之间的差异。
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;
}

+1 提及nextafter;但请注意,这种用法不太可能符合程序员的意图。假设 64 位 IEEE 754,在您的示例中 same(0, 1e-100) 返回 false,这可能不是程序员想要的。程序员可能宁愿想要一些小的阈值来测试相等性,例如 +/-1e-6 或 +/-1e-9,而不是 +/-nextafter
Y
Yakk - Adam Nevraumont

假设我们正在使用适合 16 位寄存器的玩具浮点数。有一个符号位、一个 5 位指数和一个 10 位尾数。

这个浮点数的值是尾数,解释为二进制十进制值,乘以 2 的指数次方。

在 1 附近,指数等于 0。所以尾数的最小位数是 1024 的一部分。

接近 1/2 的指数是负一,所以尾数的最小部分是原来的一半。使用 5 位指数可以达到负 16,此时尾数的最小部分值 32m 中的一部分。在负 16 指数处,该值大约是 32k 的一部分,比我们上面计算的大约 1 的 epsilon 更接近于零!

现在这是一个玩具浮点模型,它不能反映真实浮点系统的所有怪癖,但反映小于 epsilon 的值的能力与真实浮点值相当相似。


A
Arsenii Fomin

由于尾数和指数部分,您不能将此应用于 0。由于指数,您可以存储比 epsilon 小的数字,但是当您尝试执行类似 (1.0 - "very small number") 之类的操作时,您将获得 1.0。 Epsilon 不是值的指标,而是值精度的指标,以尾数表示。它显示了我们可以存储多少个正确的后继十进制数字。


c
cababunga

因此,假设系统无法区分 1.000000000000000000000 和 1.000000000000000000001。即 1.0 和 1.0 + 1e-20。你认为在 -1e-20 和 +1e-20 之间还有一些值可以表示吗?


除了零,我认为-1e-20 和+1e-20 之间没有值。但仅仅因为我认为这并不成立。
@SebastianKrysmanski:这不是真的,0 到 epsilon 之间有很多浮点值。因为它是 floating 点,而不是定点。
不同于零的最小可表示值受分配来表示指数的位数的限制。因此,如果 double 具有 11 位指数,则最小数字将是 1e-1023。
d
default

我认为这取决于您计算机的precision。看看这个table:你可以看到如果你的epsilon用double表示,但是你的精度更高,比较不等于

someValue == 0.0

总之是个好问题!


s
supercat

对于 IEEE 浮点,在最小的非零正值和最小的非零负值之间,存在两个值:正零和负零。测试一个值是否在最小的非零值之间,相当于测试是否与零相等;然而,赋值可能会产生影响,因为它会将负零变为正零。

可以想象,浮点格式可能具有介于最小有限正负值之间的三个值:正无穷小、无符号零和负无穷小。我不熟悉实际上以这种方式工作的任何浮点格式,但这样的行为将是完全合理的,并且可以说比 IEEE 的更好(也许不够好到值得添加额外的硬件来支持它,但在数学上 1 /(1/INF)、1/(-1/INF) 和 1/(1-1) 应该代表三种不同的情况,说明三个不同的零)。我不知道是否有任何 C 标准会要求有符号的无穷小,如果它们存在,则必须比较等于零。如果他们不这样做,像上面这样的代码可以有效地确保例如将一个数字重复除以 2 最终会产生零,而不是停留在“无穷小”上。


“1/(1-1)”(来自您的示例)不是无穷大而不是零吗?
数量 (1-1)、(1/INF) 和 (-1/INF) 都表示零,但是将正数除以它们中的每一个理论上应该会产生三个不同的结果(IEEE 数学认为前两个相同)。
H
Hassan Bahaloo

“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;
} 

C
Community

此外,拥有这样一个函数的一个很好的理由是删除“非规范化”(那些不能再使用隐含的前导“1”并具有特殊 FP 表示的非常小的数字)。你为什么想做这个?因为有些机器(特别是一些较旧的 Pentium 4s)在处理非规范化时会变得非常非常慢。其他人只是变得有点慢。如果您的应用程序并不真正需要这些非常小的数字,那么将它们清零是一个很好的解决方案。考虑这一点的好地方是任何 IIR 滤波器或衰减函数的最后一步。

另请参阅:Why does changing 0.1f to 0 slow down performance by 10x?

http://en.wikipedia.org/wiki/Denormal_number


这不仅删除了非规范化的数字,还删除了更多的数字。它将普朗克常数或电子质量更改为零,如果您使用这些数字,这会给您带来非常非常错误的结果。