在 C++ 中从 1 中减去极小的数

Subtract extremely small number from one in C++

我需要从 1 中减去极小的 doublex 即计算 1-x 在 C++ 中为 0。由于足够小的机器精度限制 f x 我总是会得到 1-x=1。简单的解决方案是从双精度转换为更精确的格式,如 long。但是由于某些限制,我无法切换到更精确的数字格式。

获得 1-x 准确值的最有效方法是什么,其中 x 是一个非常小的 double 如果我可以的话'不使用更精确的格式并且我需要将减法的结果存储为双精度数?在实践中,我想避免百分比误差大于 1%(在 1-x 的双重表示与其实际值之间)。

P.S. 我正在使用 Rcpp 通过 qnorm 计算标准正态分布的分位数 函数。此函数围绕 0.5 对称,对于接近 0 的值更加准确。因此,而不是 qnorm(1-(1e-30)) 我想计算 -qnorm(1e-30) 但推导 1e-30 from 1-(1e-30) 我需要处理一个精度问题。对 double 的限制是因为据我所知,在 Rcpp 中使用更精确的数字格式是不安全的。请注意,我对 qnorm 的输入可能是外生的,因此我无法从 x[= 推导出 1-x 39=] 在一些初步计算中。

Simple solution is to switch from double to some more precise format like long [presumably, double]

那你就没有办法了。 long double 是所有现代机器上 double 的别名。 我纠正了,gccicc 仍然支持它,只有 cl 有很长一段时间都放弃了对它的支持。

所以你有两个解决方案,它们并不相互排斥:

  1. 使用任意精度库而不是 built-in 类型。它们慢了几个数量级,但如果那是您的算法可以使用的最佳效果,那就是这样。

  2. 使用更好的算法,或者至少重新排列你的方程变量,从一开始就没有这个需要。使用分配和取消规则来完全避免该问题。如果没有对您的问题进行更深入的描述,我们无法为您提供帮助,但我可以肯定地告诉您,double 足以让我们在世界任何地方模拟飞机 AI 和飞行参数。

而不是求助于任意精确解决方案(正如其他人所说,可能非常慢),你可以只需创建一个 class,将 double 类型的固有精度扩展(大约)两倍。然后你只需要实现你实际需要的操作:在你的情况下,这可能只是减法(也可能是加法),这两者 合理地 很容易实现。这样的代码仍然比使用本机类型慢得多,但可能比使用不必要精度的库快得多。

这样的实现在 QD_Real class 中可用(如 open-source),由 Yozo Hida(我相信当时是博士生)创建。

链接的存储库包含 很多 代码,其中大部分可能对您的 use-case 来说是不必要的。下面,我展示了一个非常 trimmed-down 的版本,它允许创建具有所需精度的数据,显示了所需 operator-() 的实现和一个测试用例。

#include <iostream>

class ddreal {
private:
    static inline double Plus2(double a, double b, double& err) {
        double s = a + b;
        double bb = s - a;
        err = (a - (s - bb)) + (b - bb);
        return s;
    }
    static inline void Plus3(double& a, double& b, double& c) {
        double t3, t2, t1 = Plus2(a, b, t2);
        a = Plus2(c, t1, t3);
        b = Plus2(t2, t3, c);
    }
public:
    double x[2];
    ddreal() { x[0] = x[1] = 0.0; }
    ddreal(double hi) { x[0] = hi; x[1] = 0.0; }
    ddreal(double hi, double lo) { x[0] = Plus2(hi, lo, x[1]); }
    ddreal& operator -= (ddreal const& b) {
        double t1, t2, s2;
        x[0] = Plus2(x[0], -b.x[0], s2);
        t1 = Plus2(x[1], -b.x[1], t2);
        x[1] = Plus2(s2, t1, t1);
        t1 += t2;
        Plus3(x[0], x[1], t1);
        return *this;
    }
    inline double toDouble() const { return x[0] + x[1]; }
};

inline ddreal operator-(ddreal const& a, ddreal const& b)
{
    ddreal retval = a;
    return retval -= b;
}

int main()
{
    double sdone{ 1.0 };
    double sdwee{ 1.0e-42 };
    double sdval = sdone - sdwee;
    double sdans = sdone - sdval;
    std::cout << sdans << "\n"; // Gives zero, as expected

    ddreal ddone{ 1.0 };
    ddreal ddwee{ 1.0e-42 };
    ddreal ddval = ddone - ddwee; // Can actually hold 1 - 1.0e42 ...
    ddreal ddans = ddone - ddval;
    std::cout << ddans.toDouble() << "\n"; // Gives 1.0e-42

    ddreal ddalt{ 1.0, -1.0e-42 }; // Alternative initialization ...
    ddreal ddsec = ddone - ddalt;
    std::cout << ddsec.toDouble() << "\n"; // Gives 1.0e-42

    return 0;
}

请注意,我故意忽略了 error-checking 和更通用的实现所需的其他开销。此外,我展示的代码已经 'tweaked' 在 x86/x64 CPU 上更优化地工作,因此如果您需要其他支持,您可能需要深入研究链接 GitHub 处的代码平台。 (但是,我认为我展示的代码适用于任何严格符合IEEE-754标准的平台。)

我已经在我用来生成和显示 Mandelbrot 集(和相关分形)的代码中广泛地测试了这个实现 非常 深度缩放级别,其中使用原始double 类型完全失败。

请注意,尽管您可能想 'optimize' 一些 看似 无意义的操作,但这样做会破坏系统。此外,此 必须 使用 /fp:precise(或 /fp:strict)标志(使用 MSVC)或其他编译器的等效标志进行编译;使用 /fp:fast 将完全破坏代码。