精确复制浮点值的重复相加

Exactly replicating repeated addition of floating-point values

提问人:TLW 提问时间:8/21/2023 更新时间:8/29/2023 访问量:119

问:

在 IEEE 754 算术中是否存在复制浮点值重复求和的 sub-O(n) 方式1

也就是说,有没有一种更快的方法可以精确地复制以下伪代码的结果:

f64 notQuiteFMA(f64 acc, f64 f, bigint n) {
    for (bigint i = 0; i < n; i++) {
        acc = acc + f;
    }
    return acc;
}

?3

在真正的算术中,答案很简单——就足够了。然而,在有限的精度下,答案要复杂得多。举个例子,请注意,它大约是 4 2^53,而不是人们期望的无限精度的 2^256。(这个例子还说明了为什么加快这个计算速度是可取的——数到 2^256 是相当不切实际的。return acc + f*nnotQuiteFMA(0, 1, 1 << 256)

  1. 我知道从技术上讲,这整个事情可以在 O(1) 时间5 内完成,使用一个 2^128 个条目查找表,每个条目都有一个包含 2^64 个元素的数组;请忽略这样的银河系算法。(鸽子洞原理 - 重复加法在进入循环之前不能超过 2^64 步 - 所以“只是”存储整个前缀和最后一个循环中每个初始值的步数 和 。accf
  2. 在给定当前舍入模式的情况下,位精确(至少忽略不发出信号的蠕虫罐头)。
  3. 将其视为伪代码 - 即所描述的双精度浮点加法序列。我知道在某些编程语言中,浮点计算可以以更高的精度完成/重新排序以使事情对 SIMD 更友好/等等/等等;对于这个问题,我对这些情况不感兴趣。
  4. 64 位浮点数有一个 52 位尾数; 第一次发生在 2^53 左右,此时加法不足以增加尾数,结果向下舍入。我相信确切的值取决于舍入模式。x + 1 == x
  5. 假设一个计算模型,其中 bigint 的模为常数有界值至少为 O(1)。
浮点 精度 IEEE-754

评论

0赞 Tim Roberts 8/21/2023
我怀疑您的问题是针对另一个问题的拟议解决方案而出现的。你知道区间算术吗?这些库执行正常计算,但每个数字都保留上限和下限。所有的计算都是这样完成的,即使你不知道确切的答案,你也知道确切的答案在这两个边界之间。
0赞 Tim Roberts 8/21/2023
现在我看了它,我对你的问题感到困惑。你说你想完全复制那个伪代码的 IEEE-754 结果,但这不是你想要的。正如您所说,该伪代码的结果约为 ,因为在那之后,您每次都添加 0。你似乎在追求无限精度的浮点,这是矛盾的。永远不会有任何舍入错误。为什么不使用大整数呢?2^53
0赞 TLW 8/21/2023
你似乎很困惑。我不想要无限精度的结果。我希望准确地复制“不准确”的结果,而不必进行 O(n) 加法。
0赞 TLW 8/21/2023
这里的“准确复制”不准确“的结果”,意思是“得出与所描述的朴素 O(n) 求和相同的位精确结果”。
0赞 TLW 8/21/2023
“你说你想完全复制那个伪代码的IEEE-754结果,但这不是你想要的。这正是我想要的。

答:

-2赞 Tim Roberts 8/21/2023 #1

我可以回答这个问题,尽管我认为你不会喜欢它。

一旦数字滚动到 53 位有效位,该值将停止递增。我们可以计算出来。下面是 Python 中的一个脚本,它显示了向浮点数添加 1 不再更改浮点数的点:

import struct

j = 1
for i in range(1,58):
    j = 2*j
    r = float(j)
    s = r+1
    if r == s:
        pat = struct.pack('>d',r)
        print(pat.hex())
        print(i,j,r,s)
        break

输出:

4340000000000000
53 9007199254740992 9007199254740992.0 9007199254740992.0

第一行是该值的十六进制表示形式。因此,它是指数为 52 的隐藏 1。

评论

0赞 chtz 8/21/2023
我想你不明白OP的问题。OP 知道,对于任何一个点来说,在浮点数学中都足够大。faccacc + f == acc
0赞 Tim Roberts 8/22/2023
我不同意。他问有没有办法精确地复制他最初的伪代码的结果,而不需要 2^256 个循环。我确实做到了这一点。在每种情况下,当总和和加法指数相差 52 时,循环就不再有用。
0赞 chtz 8/22/2023
OP 希望排除 LUT,这将导致算法。但可以肯定的是,从技术上讲你是对的,在技术上与 相同,但实际上,它仍然是 .2^256O(1)O(min(2^53,n))O(1)O(n)
0赞 Tim Roberts 8/23/2023
我想我试图提出一个不同的观点。我并不是真的提出这个算法作为解决他问题的方法。我试图指出,有一种确定指数失配点的分析方法,其中丢弃了额外的加法,根本不做任何计算。
0赞 aka.nice 8/21/2023 #2

这是一个棘手的问题......

至少我们知道,直到 ,结果总是处于四舍五入到最接近的偶数模式。
例如,参见 3*x+x 总是准确的吗?
n=5n*f

但后来我们逐渐失去了精确度......以什么速度?

我认为我们可以预测溢出的情况,所以我不会为此烦恼。因此,我们可以忽略指数部分,而专注于有效部分。

渐进下溢的情况也不是很有趣,因为在偏置指数达到 1 之前,加法将是精确的。

因此,我们可以关注 (1,2( 区间中的有效数,1 表示无趣。

我尝试遵循 Smalltalk 片段以了解错误边界:

sample := Array new: 60000.
x := 1.0.
00001 to: 10000 do: [:i | sample at: i put: (x := x successor)].
x := 2.0.
10001 to: 20000 do: [:i | sample at: i put: (x := x predecessor)].
x := 1.5.
20001 to: 30000 do: [:i | sample at: i put: (x := x predecessor)].
x := 1.5.
30001 to: 40000 do: [:i | sample at: i put: (x := x successor)].
x := 1.125.
40001 to: 50000 do: [:i | sample at: i put: (x := x predecessor)].
x := 1.125.
50001 to: 60000 do: [:i | sample at: i put: (x := x successor)].
(3 to: 10) collect: [:i |
    | setOfErrors |
    setOfErrors := sample collect: [:f | ((1 to: 1<<i) inject: 0 into: [:acc :ignored | acc+f]) - (1<<i*f) / f ulp] as: Set.
    {i. setOfErrors size. setOfErrors max log2}].

1<<i是左移,所以等价于 .
因此,当 n 是 2 的幂时,对于上述有效样本,误差范围为:
2^i

 #(#(3 5 4.0) #(4 9 6.0) #(5 17 8.0) #(6 33 10.0) #(7 65 12.0) #(8 129 14.0) #(9 257 16.0) #(10 513 18.0)) 

这是 时的最大误差,以及所有可能的误差组合。2^(2*(i-1))*ulp(f)n=2^i2^(i-1)+1k*2^i*ulp(f)k=-2^(i-2)...2^(i-2)

在我们达到 2 提高到有效位数之前,有许多不同的可能性......因此,我怀疑您是否可以找到位模式的任何简单功能,仅适用于默认舍入模式。

编辑

使用 Squeak Smalltalk 的 ArbitraryPrecisionFloat 包,我尝试用 p 有效位推广 Float 的公式。

例如,使用 11 位有效:

p := 11.
(3 to: p + 1) collect: [:power |
    | n setOfErrors |
    n := 1 << power.
    setOfErrors := (1 << p to: 2<<p - 1) collect: [:significand |
        | f acc |
        f := (significand asArbitraryPrecisionFloatNumBits: p) timesTwoPower: 0 - p.
        acc := 0. n timesRepeat: [acc := acc + f].
        (n*f-acc/(acc ulp)) asFraction] as: Set.
    {power. setOfErrors max log2 printShowingDecimalPlaces: 2. setOfErrors size}].

我们得到这个结果:

#(#(3 '1.00' 5) #(4 '2.00' 9) #(5 '3.00' 17) #(6 '3.91' 32) #(7 '4.91' 61) #(8 '5.88' 120) #(9 '6.77' 220) #(10 '7.41' 354) #(11 '7.41' 512) #(12 '10.00' 1024))  

请注意,过去的操作数,总和不会再改变。2^(p+1)

到目前为止,存在与上一个样本所示的不同结果。power=floor(p/2)2^(power-1)+1

然后,不同结果的数量增加得更慢,但最后,它达到 ,即每 1 个不同的有效数有 2 个不同的结果。2^(p-1)

这表明这将是相当复杂的。notQuiteFMA

询问 的模式,我们看到它似乎取决于 f 有效性的最后一位,直到总和达到一个新的 binade......(n*f-acc/(acc ulp))ceil(log2(n))

所以,也许你可以问这样的话:

  • 的 Binade(指数)与 的 Binade(指数)是多少?n*ff
  • 对于 F 的后继者,误差序列是否遵循可预测的模式?n*(f+k*ulp(f))

如果你能肯定地回答,那么你就有机会设计出一个加速功能......notQuiteFMA

评论

0赞 Eric Postpischil 8/22/2023
回复“这是一个棘手的问题”:呵呵,这很容易。
1赞 aka.nice 8/22/2023
@EricPostpischil啊,是的,一旦你看到在每次浮点加法时都添加了完全相同的数量,直到累加器通过下一个二进制,这就会变得更简单。很好的答案。
5赞 Eric Postpischil 8/22/2023 #3

结论

我们可以通过单步执行 binades 来实现 O(log n) 解决方案。这是因为一个 binade 中的所有加法,可能除了第一个加法之外,都会以相同的量改变累积总和,因此我们可以很容易地准确地计算出重复加法会产生什么总和,直到 binade 结束。

预赛

binade 是某个整数 e 的区间 [2^e, 2^e+1) 或 (−2^e+1, −2e]。就此答案而言,整个最小指数范围(包括最小指数法线和所有次正态值)将被视为单个二元组。

对于浮点数 x(可以浮点格式表示的数字),将 E(x) 定义为表示 x 时使用的指数。鉴于使用此答案的 binade 的定义,E(x) 表示 binade x 在,除了它的符号。

代码格式的表达式表示浮点运算。x+y将 xy 相加的实数算术结果。 是将 和 相加的浮点结果。x 并用于适当的相同值。x+yxyx

浮点加法对于有限值是弱单调的:< 表示 <= ,在任何舍入模式下,除非是无穷大和/或为相反符号的无穷大,从而产生 NaN。yzx+yx+zxyz

讨论

设 A 0、A 1A2 是 执行 之前、之后的值。 考虑 E(A 0) ≥ E(A 1) = EA2) 的情况。gA2A1g 是可表示的,如果 A 1 是正态的,或者如果 A 1 是次正态的,则由 Sterbenz 引理表示,因为 A 1、A 2 和都是次正态(或零),并且没有低于 A2 的 ULP 的位在加法中丢失。(由于 g 是可表示的,因此在计算时不会出现四舍五入误差。AA += ffA2-A1

我们将证明,所有后续的相加都会产生 E() = E(A1) 的值增加相同的量 gfAAA

uA1 的 ULP。设 r 是 ||模 U.如果 f 为正数,则 s 为 +1,如果 f负数,则为 −1。(f 为零的情况是微不足道的。使用有向舍入模式(朝向 −∞、朝向零或朝向 +∞),根据舍入方法和 的符号,任何不改变 E() 的后续加法中的舍入误差始终为 −r 或始终为 ur使用四舍五入到最接近的领带 如果 r < 1/2u,则舍入误差始终为 −sr,否则始终为 sur)。当四舍五入到最接近时,平局为偶数,如果 r ≠ 1/2u,则舍入误差始终为 −sr 或始终为 sur),与平局为 away 一样。fAf

这样一来,四舍五入到最接近的,与 r = 1/2u 有偶数的联系。设 bu 位置的 f 位。在这种情况下,A 1 的低位必须是偶数,因为它是将剩余模 u 为 1/2 u 的 f 与 A 0(必然是 0 模 u,因为 E(A 0) ≥ E(A 1))相加以产生 A 1 的结果.此外,实数结果正好介于两个可表示值之间,因此它被四舍五入到一个偶数(2u 的倍数)。

假设 A1 的低位是偶数,我们有两种情况:

b 为 0,因此加法的实数算术结果是 0 u + 1/2 u 模 2 u ≡ 1/2 u,并且四舍五入到偶数,因此如果结果为正,则舍入误差为 −1/2 u,如果结果为负,则舍入误差为 +1/2u然后对后续添加重复此操作,生成 e () = E(A1)。fAA

b 为 1,因此加法的实数算术结果是 1 u + 1/2 u 模 2 u ≡ 11/2 u,并且四舍五入到偶数,因此如果结果为正,则舍入误差为 +1/2 u,如果结果为负,则舍入误差为 −1/2 u然后对后续添加重复此操作,生成 e () = E(A1)。fAA

(要了解为什么我们在断言所有后续加法都加相同量之前先进行二次加法,请观察产生 A0 的加法可能涉及具有较低 ULP 的先验位,在这种情况下,实数算术结果可能并不完全介于两者之间 可表示的值,因此 A 0 可以有一个奇数的低位数偶数,使用四舍五入到最接近,并列到偶数,并且 A 2-A 1 不等于 A 1-A 0 A

实现

鉴于上述情况,我们可以计算出在达到下一个 binade 之前可以执行多少次 g,比如 t。然后我们可以计算出 A 在退出当前 binade 之前的确切值 A+tg。然后,一个加法为我们提供了该比合体中的第一个 A,另一个加法要么将我们带入一个新的加法,要么为我们提供执行当前比合计算所需的信息。然后,我们可以对每个 binade 重复此操作,直到达到所需的添加次数。

下面是演示该概念的代码。根据评论,此代码需要一些改进。我希望在时间允许的情况下进行研究,这可能比现在还要几周以上。代码中的函数从 中获取指数,而不是如上面讨论中所述将其限制为最小指数。这可能会减慢代码速度,但不应使其不正确。我希望代码处理次正常现象。无限和 NaN 可以通过普通测试添加。Efrexp

t 的计算,即在退出当前 binade 之前可以执行的加法次数,可以使用更多关于所需精度以及舍入如何影响它的讨论。

#include <math.h>
#include <stdio.h>
#include <stdlib.h>


static float Sum0(float A, float f, int n)
{
    if (n < 0) { f = -f, n = -n; }
    while (n--) A += f;
    return A;
}


//  Return the exponent of x, designating which binade it is in.
static int E(float x)
{
    int e;
    frexp(x, &e);
    return e;
}


static float Sum1(float A, float f, int n)
{
    if (n < 0) { f = -f, n = -n; }

    if (n == 0) return A;

    while (1)
    {
        //  Record initial exponent.
        int e0 = E(A);

        A += f;
        if (--n == 0) return A;

        //  If exponent changed, continue to do another step.
        if (E(A) != e0) continue;

        /*  Note it is possible that A crossed zero here and is in the "same"
            binade with a different sign.  With rounding modes toward zero or
            round to nearest with ties to away, the rounding caused by adding f
            may differ.  However, if we have crossed zero, the next addition
            will put A in a new binade, and the loop will exit this iteration
            and start a new step.
        */

        float A0 = A;
        A += f;
        if (--n == 0) return A;

        //  If exponent changed, continue to do another step.
        if (E(A) != e0) continue;

        //  Calculate exact change from one addition.
        float g = A - A0;

        //  If g is zero, no future additions will change the sum.
        if (g == 0) return A;

        /*  Calculate how many additions can be done until just before the
            next binade.

            If A and f have the same sign, A is increasing in magnitude, and
            the next binade is at the next higher exponent.  Otherwise, the
            next binade is at the bottom of the current binade.

            We use copysign to get the right sign for the target.  .5 is used
            with ldexpf because C's specifications for frexp and ldexpf
            normalize the significand to [.5, 1) instead of IEEE-754's [1, 2).
        */
        int e1 = e0 + (signbit(A) == signbit(f));

        double t = floor((ldexpf(copysignf(.5, A), e1) - A) / (double) g);

        /*  Temporary workaround to avoid incorrectly steppping to the edge of
            a lower binade:
        */
        if (t == 0) continue;
        --t;

        if (n <= t) return A + n*g;

        A += t*g;
        n -= t;
    }
}


static int Check(float A, float f, int n)
{
    float A0 = Sum0(A, f, n);
    float A1 = Sum1(A, f, n);
    return A0 != A1;
}


static void Test(float A, float f, int n)
{
    float A0 = Sum0(A, f, n);
    float A1 = Sum1(A, f, n);
    if (A0 != A1)
    {
        printf("Adding %a = %.99g to %a = %.99g %d times:\n", f, f, A, A, n);
        printf("\tSum0 -> %a = %.99g.\n", A0, A0);
        printf("\tSum1 -> %a = %.99g.\n", A1, A1);
        exit(EXIT_FAILURE);
    }
}


int main(void)
{
    srand(1);

    for (int i = 0; i < 1000000; ++i)
    {
        if (i % 10000 == 0) printf("Tested %d cases.\n", i);
        Test(rand() - RAND_MAX/2, rand() - RAND_MAX/2, rand() % (1<<20));
    }
}

评论

0赞 Eric Postpischil 8/22/2023
@njuffa:我认为一般的算法是:循环开始:A += f。如果 binade 已更改,请转到“开始”。A0 = A,A += f。如果 binade 已更改,请转到“开始”。否则设置 g = A-A0,计算添加次数 t 直到下一个 binade,设置 A += t*g.(在每一步插入测试,当我们达到所需的添加次数时停止。我认为这适用于任何起始 A 值以及 A 和 F 的符号和大小的任何组合。基本前提是 binade 中的第二次和后续加法对 A 有固定的变化,因此我们可以计算整个 binade。
1赞 Eric Postpischil 8/22/2023
@njuffa:我的代码涵盖了不包括无穷大和 NaN 的一般情况——任何起始累加器、任何 f,无论符号和相对大小如何。它正在运行,但我需要进一步测试它并写下来。也许以后,现在有差事要做。我将其作为评估的候选代码添加到答案中。
1赞 njuffa 8/24/2023
别着急。我不是故意制造任何压力,只是表示兴趣。虽然我没有立即需要此功能,但 (1) 它似乎绝对有用,并且 (2) 经过一番思考后,我怀疑是否能找到通用解决方案(我很高兴得知我的评估可能不正确:-)
1赞 Eric Postpischil 8/27/2023
@TLW:如果没有,等我做完了。对此,亚正常现象并不难。
1赞 njuffa 8/29/2023
@TLW 广泛的测试不是证明,但就其价值而言,我没有观察到由于我在之前的评论中链接的略微修改的代码的次正常现象而导致的测试失败。