15

我正在尝试编写一个程序,将 π 的十进制数字计算为 1000 位或更多位。

为了有趣地练习低级编程,最终程序将用汇编编写,在没有乘法或除法的 8 位 CPU 上,只执行 16 位加法。为了简化实现,希望能够仅使用 16 位无符号整数运算,并使用迭代算法。速度不是主要问题。而快速乘法和除法超出了这个问题的范围,所以也不要考虑这些问题。

在汇编中实现它之前,我仍然试图在我的台式计算机上找出一个可用的 C 算法。到目前为止,我发现以下系列相当有效且相对容易实现。

该公式是使用收敛加速技术从莱布尼茨系列推导出来的,要推导它,请参阅 Carl D. Offner 的 Computing the Digits in π ( https://cs.umb.edu/~offner/files/pi.pdf ) ,第 19-26 页。最终公式见第26页。我写的初始公式有一些拼写错误,请刷新页面查看固定公式。最大项的常​​数项2在第 54 页中进行了解释。该论文还描述了一种高级迭代算法,但我在这里没有使用它。

计算 π 的系列(固定错字)

如果使用许多(例如 5000)项评估该系列,则可以轻松获得数千位 π,并且我发现该系列也很容易使用此算法进行迭代评估:

算法

  1. 首先,重新排列公式以从数组中获取其常数项。

重新排列的公式(修复了另一个错字)

  1. 用 2 填充数组以开始第一次迭代,因此新公式类似于原始公式。

  2. carry = 0.

  3. 从最大的术语开始。从数组中获取一项 (2),将该项乘以PRECISION对 执行定点除法2 * i + 1,并将提醒作为新项保存到数组中。然后添加下一个术语。现在递减i,进入下一个学期,重复直到i == 1。最后加上最后一个词x_0

  4. 因为使用了 16 位整数,PRECISION所以10得到 2 个十进制数字,但只有第一个数字有效。将第二个数字保存为进位。显示第一个数字加进位。

  5. x_0是整数 2,不应该为连续迭代添加,清除它。

  6. 转到第 4 步以计算下一个十进制数字,直到我们拥有所需的所有数字。

实施1

将此算法转换为 C:

#include <stdio.h>
#include <stdint.h>

#define N 2160
#define PRECISION 10

uint16_t terms[N + 1] = {0};

int main(void)
{
    /* initialize the initial terms */
    for (size_t i = 0; i < N + 1; i++) {
        terms[i] = 2;
    }

    uint16_t carry = 0;
    for (size_t j = 0; j < N / 4; j++) {
        uint16_t numerator = 0;
        uint16_t denominator;
        uint16_t digit;

        for (size_t i = N; i > 0; i--) {
            numerator += terms[i] * PRECISION;
            denominator = 2 * i + 1;

            terms[i] = numerator % denominator;
            numerator /= denominator;
            numerator *= i;
        }
        numerator += terms[0] * PRECISION;
        digit = numerator / PRECISION + carry;
        carry = numerator % PRECISION;

        printf("%01u", digit);

        /* constant term 2, only needed for the first iteration. */
        terms[0] = 0;
    }
    putchar('\n');
}

该代码可以将 π 计算为 31 位十进制数字,直到出错。

31415926535897932384626433832794
10 <-- wrong

有时digit + carry大于 9,因此需要额外的进位。如果我们运气不好,甚至可能出现双进位、三进位等。我们使用环形缓冲区来存储最后 4 位数字。如果检测到一个额外的进位,我们输出一个退格来擦除前一个数字,执行一个进位,然后重新打印它们。这只是概念证明的一个丑陋的解决方案,这与我关于溢出的问题无关,但为了完整起见,这就是它。将来会实施更好的东西。

重复进位的实现 2

#include <stdio.h>
#include <stdint.h>

#define N 2160
#define PRECISION 10

#define BUF_SIZE 4

uint16_t terms[N + 1] = {0};

int main(void)
{
    /* initialize the initial terms */
    for (size_t i = 0; i < N + 1; i++) {
        terms[i] = 2;
    }

    uint16_t carry = 0;
    uint16_t digit[BUF_SIZE];
    int8_t idx = 0;

    for (size_t j = 0; j < N / 4; j++) {
        uint16_t numerator = 0;
        uint16_t denominator;

        for (size_t i = N; i > 0; i--) {
            numerator += terms[i] * PRECISION;
            denominator = 2 * i + 1;

            terms[i] = numerator % denominator;
            numerator /= denominator;
            numerator *= i;
        }
        numerator += terms[0] * PRECISION;
        digit[idx] = numerator / PRECISION + carry;

        /* over 9, needs at least one carry op. */
        if (digit[idx] > 9) {
            for (int i = 1; i <= 4; i++) {
                if (i > 3) {
                    /* allow up to 3 consecutive carry ops */
                    fprintf(stderr, "ERROR: too many carry ops!\n");
                    return 1;
                }
                /* erase a digit */
                putchar('\b');

                /* carry */
                digit[idx] -= 10;
                idx--;
                if (idx < 0) {
                    idx = BUF_SIZE - 1;
                }
                digit[idx]++;            
                if (digit[idx] < 10) {
                    /* done! reprint the digits */
                    for (int j = 0; j <= i; j++) {
                        printf("%01u", digit[idx]);
                        idx++;
                        if (idx > BUF_SIZE - 1) {
                            idx = 0;
                        }
                    }
                    break;
                }
            }
        }
        else {
            printf("%01u", digit[idx]);
        }

        carry = numerator % PRECISION;
        terms[0] = 0;

        /* put an element to the ring buffer */
        idx++;
        if (idx > BUF_SIZE - 1) {
            idx = 0;
        }
    }
    putchar('\n');
}

太好了,现在程序可以正确计算 534 位 π,直到出错。

3141592653589793238462643383279502884
1971693993751058209749445923078164062
8620899862803482534211706798214808651
3282306647093844609550582231725359408
1284811174502841027019385211055596446
2294895493038196442881097566593344612
8475648233786783165271201909145648566
9234603486104543266482133936072602491
4127372458700660631558817488152092096
2829254091715364367892590360011330530
5488204665213841469519415116094330572
7036575959195309218611738193261179310
5118548074462379962749567351885752724
8912279381830119491298336733624406566
43086021394946395
22421 <-- wrong

16 位整数溢出

事实证明,在开始计算最大项的过程中,误差项变得非常大,因为开始的除数在 ~4000 的范围内。在评估级数时,numerator实际上立即开始在乘法中溢出。

整数溢出在计算前 500 位时是微不足道的,但开始变得越来越糟,直到给出不正确的结果。

更改uint16_t numerator = 0uint32_t numerator = 0可以解决这个问题并将 π 计算到 1000+ 位。

但是,正如我之前提到的,我的目标平台是 8 位 CPU,并且只有 16 位操作。是否有一个技巧可以解决我在这里看到的 16 位整数溢出问题,只使用一个或多个 uint16_t?如果无法避免多精度算术,那么在这里实现它的最简单方法是什么?我知道我需要引入一个额外的 16 位“扩展字”,但我不确定如何实现它。

并提前感谢您耐心地理解这里的长篇大论。

4

3 回答 3

2

看看相关的QA:

它使用Wiki:Bailey–Borwein–Plouffe_formula,它更适合整数运算。

然而,真正的挑战是:

因为您可能想以 dec base 打印数字...

此外,如果您需要使用比 asm 更高级别的语言,请查看以下内容:

您可以修改它以根据需要处理尽可能多的进位位(如果仍然小于数据类型位宽)。

[Edit1] C++/VCL 中的 BBP 示例

我使用了这个公式(取自上面链接的 Wiki 页面):

血压

转换为固定点...

//---------------------------------------------------------------------------
AnsiString str_hex2dec(const AnsiString &hex)
    {
    char c;
    AnsiString dec="",s;
    int i,j,l,ll,cy,val;
    int  i0,i1,i2,i3,sig;
    sig=+1; l=hex.Length();
    if (l) { c=hex[l]; if (c=='h') l--; if (c=='H') l--; }
    i0=0; i1=l; i2=0; i3=l;
    for (i=1;i<=l;i++)      // scan for parts of number
        {
        char c=hex[i];
        if (c=='-') sig=-sig;
        if ((c=='.')||(c==',')) i1=i-1;
        if ((c>='0')&&(c<='9')) { if (!i0) i0=i; if ((!i2)&&(i>i1)) i2=i; }
        if ((c>='A')&&(c<='F')) { if (!i0) i0=i; if ((!i2)&&(i>i1)) i2=i; }
        if ((c>='a')&&(c<='f')) { if (!i0) i0=i; if ((!i2)&&(i>i1)) i2=i; }
        }

    l=0; s=""; if (i0) for (i=i0;i<=i1;i++)
        {
        c=hex[i];
             if ((c>='0')&&(c<='9')) c-='0';
        else if ((c>='A')&&(c<='F')) c-='A'-10;
        else if ((c>='a')&&(c<='f')) c-='A'-10;
        for (cy=c,j=1;j<=l;j++)
            {
            val=(s[j]<<4)+cy;
            s[j]=val%10;
            cy  =val/10;
            }
        while (cy>0)
            {
            l++;
            s+=char(cy%10);
            cy/=10;
            }
        }
    if (s!="")
        {
        for (j=1;j<=l;j++) { c=s[j]; if (c<10) c+='0'; else c+='A'-10; s[j]=c; }
        for (i=l,j=1;j<i;j++,i--) { c=s[i]; s[i]=s[j]; s[j]=c; }
        dec+=s;
        }
    if (dec=="") dec="0";
    if (sig<0) dec="-"+dec;

    if (i2)
        {
        dec+='.';
        s=hex.SubString(i2,i3-i2+1);
        l=s.Length();
        for (i=1;i<=l;i++)
            {
            c=s[i];
                 if ((c>='0')&&(c<='9')) c-='0';
            else if ((c>='A')&&(c<='F')) c-='A'-10;
            else if ((c>='a')&&(c<='f')) c-='A'-10;
            s[i]=c;
            }
        ll=((l*1234)>>10);  // num of decimals to compute
        for (cy=0,i=1;i<=ll;i++)
            {
            for (cy=0,j=l;j>=1;j--)
                {
                val=s[j];
                val*=10;
                val+=cy;
                s[j]=val&15;
                cy=val>>4;
                }
            dec+=char(cy+'0');
            for (;;)
                {
                if (!l) break;;
                if (s[l]) break;
                l--;
                }
            if (!l) break;;
            }
        }

    return dec;
    }
//---------------------------------------------------------------------------
AnsiString pi_BBP() // https://en.wikipedia.org/wiki/Bailey–Borwein–Plouffe_formula
    {
    const int N=100;        // 32*N bit uint arithmetics
    int sh;
    AnsiString s;
    uint<N> pi,a,b,k,k2,k3,k4;

    for (pi=0,sh=(N<<5)-8,k=0;sh>=0;k++,sh-=4)
        {
        k2=k*k;
        k3=k2*k;
        k4=k3*k;
        a =k2* 120;
        a+=k * 151;
        a+=     47;
        b =k4* 512;
        b+=k3*1024;
        b+=k2* 712;
        b+=k * 194;
        b+=     15;
        a<<=sh;
        pi+=a/b;
        }
    pi<<=4;
    s=pi.strhex();
    s=s.Insert(".",2);
    return str_hex2dec(s);
    }
//---------------------------------------------------------------------------

该代码使用VCL AnsiString,它是一个自分配字符串和我的模板,它是基于我的ALU32uint<N>的位宽的无符号整数算术。如您所见,您只需要大整数除法加法和乘法即可(所有其他东西都可以在普通整数上使用)。32*N

这里十进制结果与 1000 位 Pi 参考:

ref: 3.1415926535897932384626433832795028841971693993751058209749445923078164062862089986280348253421170679821480865132823066470938446095505822317253594081284811174502841027019385211055596446229489549303819644288109756659334461284756482337867831652712019091456485669234603486104543266482133936072602491412737245870066063155881748815209209628292540917153643678925903600113305305488204665213841469519415116094330572703657595919530921861173819326117931051185480744623799627495673518857527248912279381830119491298336733624406566430860213949463952247371907021798609437027705392171762931767523846748184676694051320005681271452635608277857713427577896091736371787214684409012249534301465495853710507922796892589235420199561121290219608640344181598136297747713099605187072113499999983729780499510597317328160963185950244594553469083026425223082533446850352619311881710100031378387528865875332083814206171776691473035982534904287554687311595628638823537875937519577818577805321712268066130019278766111959092164201989
BPP: 3.14159265358979323846264338327950288419716939937510582097494459230781640628620899862803482534211706798214808651328230664709384460955058223172535940812848111745028410270193852110555964462294895493038196442881097566593344612847564823378678316527120190914564856692346034861045432664821339360726024914127372458700660631558817488152092096282925409171536436789259036001133053054882046652138414695194151160943305727036575959195309218611738193261179310511854807446237996274956735188575272489122793818301194912983367336244065664308602139494639522473719070217986094370277053921717629317675238467481846766940513200056812714526356082778577134275778960917363717872146844090122495343014654958537105079227968925892354201995611212902196086403441815981362977477130996051870721134999999837297804995105973173281609631859502445945534690830264252230825334468503526193118817101000313783875288658753320838142061717766914730359825349042875546873115956286388235378759375195778185778048187

计算的 bigint 值导出为十六进制字符串,然后使用str_hex2dec上面的链接转换为十进制基数。迭代次数取决于目标位宽。

代码还没有优化...

于 2019-05-08T07:07:54.953 回答
1

如何实现 32 位算术?

对于加法,将两个高位字(16 位)相加,然后是两个低位字,测试溢出位,必要时进位到高位结果。

如果您可以预测何时会发生溢出,您可以在必要时从 16 位运算切换到 32 位运算。


无法在纯 C 中测试溢出位,它将需要一些内联汇编或内部函数。

否则,您可以从这个答案中得到启发:https ://codereview.stackexchange.com/a/37178/39646

于 2019-05-07T13:39:07.647 回答
0

有个窍门:

考虑为分子使用一个数组,为分母使用另一个数组。每个位置将代表该数字乘以得到实际数字的次数。

一个例子:

(1 * 2 * 3 * 7 * 7) / (3 * 6 * 8)

将表示为:

num[] = {1, 1, 1, 0, 0, 0, 2};
denom[] = {0, 0, 1, 0, 0, 1, 0, 1};

然后考虑在存储之前将每个数字分解为素数,这样你就有更少的数字。现在您将需要另一个数组来存储所有素数:

primes[] = {2, 3, 5, 7};
num[] = {1, 1, 0, 2};
denom[] = {4, 2, 0, 0};

这将允许您存储难以想象的大数字,但您迟早会想要将它们转换回数字,因此您需要先简化它。做到这一点的方法只是减去factors[i] += num[i] - denom[i]数组中的每个字段,减去系列中的每个分数。您将希望在每次迭代后进行简化,从而最大限度地降低溢出风险。

factors[] = {-3, -1, 0, 2};

当您需要数字时,只需对数组中的每个字段num *= pow(primes[i], factors[i]);的因子为正数或负数即可。num /= pow(primes, -factors[i]);(如果为 0,则不执行任何操作。


num并且denom是用于存储分数的临时数组,存储结果的数组是factors. 每次使用前请记住memset临时数组。


这种解释对任何大的部分都是有用的。为了适应您的具体问题,您可能需要使用整数幂函数,并乘以 10^something 将小数部分转换为整数部分。那是你的使命,你应该接受吗:)

于 2019-05-07T13:03:35.653 回答