只有整数部分的 C# 上的算术运算是否总是产生只有整数部分的代数一致的结果?

Does arithmetic on C# doubles with only integral parts always produce algebraically consistent results that have only integral parts?

这个网站上有很多很棒的答案解释了对带小数部分的 C# double 进行简单算术运算可能会导致严重的双重相等问题,即两个在代数上明显相等的计算结果被视为不相等比较相等性时,即使只完成双对象的加法、减法和乘法(没有除法),但如果有两个双精度算术表达式,其中所有双精度都具有,是否仍然存在严重的双精度问题的可能性只有整数部分没有小数部分?

也就是说,C# 上的算术运算(不包括除法)是否只有整数部分产生的结果也只有整数部分,例如两个这样的表达式,如 3.0d + (2.0d * 3.0d * 5.0d) - 2.0d和 2.0d + (10.0d * 3.0d) - 1.0d,它们在代数上是相等的,可以安全地与 if (... == ...) 条件进行比较,或者可以将任意 double 集合的此类算术表达式进行比较文字,包括那些具有非常大的值的文字,以某种方式评估为略有不同的数字,包括两个结果具有不同整数部分的可能性,只要结果及其中间计算保持在 - 9,007,199,254,740,992 和 9,007,199,254,740,992,这是 C# 中的最小值和最大值,超过该值后每个 64 位整数都不能再用双精度数表示?

不一定。

机器精度

浮点数在内部表示(根据 IEEE 754)如下

x = s*(1 + m*2^(-52))*2^(k-1023)

其中 mk 是整数,m 为 52 位,k 为 11 位。 s 是符号位,它是 -1+1。对于 post 的其余部分,我忽略了这一点,因为我假设 x 是正数。

您可以检测到的值 x 的最小变化是

eps(x) = 2^(floor(log(x,2))-52)

例如x=40000000就是

40000000 = (1+865109492629504*2^(-52))*2^(1048-1023)
         = (1+0.192092895507812)*2^25
         = (1.192092895507812)*33554432

下一个最大的数字在 m

中有 +1
next = (1+865109492629505*2^(-52))*2^(1048-1023)
     = (1+0.19209289550781272)*2^25
     = 40000000.000000007

x的精度(等于到下一个的数值距离)是

eps = 2^(floor(log(x,2))-52)
    = 2^(25-52)
    = 2^(-27)
    = 0.000000007450580596923828125

因此,任何 double 值是否为非小数,都会有一定的相关精度,每次计算都会变得更糟。

这意味着如果你用上面的x做134217728次计算,你最终会得到>1.0的不确定性。这并不牵强。每次迭代 11,600 次的双循环,您就可以到达那里。

限制示例是x=9007199254740992.0

此数字的精度为 eps(x)=2.0,因此将 1.0 添加到 x 不会产生预期的结果。

您应该使用上面的 eps(x) 函数来确定两个数字在精度方面的接近程度。

附录

一个数字的composition/decomposition可以用下面的代码检查:

    static double Compose(byte s, ulong m, short k)
    {
        return s*(1.0+ m*Math.Pow(2, -52))*Math.Pow(2, k-1023);
    }

    static void Decompose(double x, out byte s, out ulong m, out short k)
    {
        s = (byte)Math.Sign(x);
        x = Math.Abs(x);
        k = (short)(Math.Log(x, 2)+1023);
        var z = x/Math.Pow(2, k-1023)-1;
        m = (ulong)(4503599627370496*z);
    }
    static double Eps(double x)
    {
        x = Math.Abs(x);
        if (x==0) return Eps(1.0);
        return Math.Pow(2, Math.Floor(Math.Log(x, 2))-52);
    }

我不明白为什么 C# 使用 double.Epsilon = 2^(-2074) 并且没有像 Julia 和其他语言那样的 Eps(x) 的内置函数。