BBP公式计算十万位十六进制圆周率(C语言)

BBP公式简介

贝利-波尔温-普劳夫公式(BBP 公式) 提供了一个计算圆周率π的第n位二进制数的算法。于1985年被提出。其衍生出的贝拉公式,是当前世界上计算机实现圆周率计算效率最高的公式之一。公式的形式如下。

公式的形式似乎不是很优美。但是摁一下计算器就可以发现,收敛的速度是很快的。注意到式子中很适合计算机二进制,这也是我们快速求圆周率十六进制表示的关键。

为了求前面位十六进制的圆周率,首先可以将式子两头乘以,得到:
16^n\pi=\sum_{k=0}^{n}\left[\frac{4\cdot{16^{n-k}}}{8 k+1}-\frac{2\cdot{16^{n-k}}}{8 k+4}-\frac{1\cdot{16^{n-k}}}{8 k+5}-\frac{1\cdot{16^{n-k}}}{8 k+6}\right]\\+\sum_{k=n+1}^{\infty}\left[\frac{1}{16^{k-n}}\left(\frac{4}{8 k+1}-\frac{2}{8 k+4}-\frac{1}{8 k+5}-\frac{1}{8 k+6}\right)\right]

左边式子可以看做的十六进制表示下,小数点向右侧位移了次,整数部分,就是我们想要的答案。

可以看出,右边式子前面的项对于前面位有决定性意义。而后面一项则有影响,但是影响不大。

如果将第一个求和项中的除法全部换成向下的整除,并且省略掉第二个求和项。会对末尾的几位造成较大影响。因此一般会选择将实际取的增大一些,例如我们今天要求,稍微安全点就应该取,多求八位,还不保险就取更大一些,然后得到答案时就取前面即可。

剩下的第一个求和项,实现起来就并不困难了。

Python 实现

在用C语言实现时,我们先用Python打一下草稿。

import time
f=open("/home/max/桌面/2.txt","w")
ans=0
v=100008
sta=time.time()
tmp=16**(v)
for k in range(v):
    a=tmp*4//(8*k+1)
    b=tmp*2//(8*k+4)
    c=tmp//(8*k+5)
    d=tmp//(8*k+6)
    ans+=a-b-c-d
    tmp//=16
ed=time.time()
print(ed-sta)
f.write(hex(ans)[2:-8])
f.close()

Python的优点是非常便于实现。自带的大整数实际表现也非常高效。

本机运行大概,就成功求出了答案。

C语言

C语言需要自己实现大整数的运算。但实际上,我们发现只有大整数相加,相减,和整除以一个很小的数字。而可以不用到大整数相乘相除。

为了高效性和便捷性,我们应该充分应用存储数组的每一比特。例如是类型存储,就应该是进制。这样涉及到运算的位数就会尽量少,最后输出时,也只需要用的标识。

为了可能性的延拓,先定义好一些可替换的标识符和常量:

#define LIM 100008
#define ARRAY_SZ 12505  //>LIM*4/SZ_BASE_t

typedef unsigned int BASE_t;
typedef unsigned int ITER_t;
typedef unsigned long long DOUBLE_BASE_t;

const ITER_t SZ_BASE_t = sizeof(BASE_t) * 8;
const ITER_t ITER_END = (ITER_t) (-1);
const BASE_t var1[4] = {2, 1, 0, 0};
const BASE_t var2[4] = {1, 4, 5, 6};
const BASE_t BASE_INF = (BASE_t) (-1);

这里代表了之前的。根据我们计算出一个大数存储所需的。

下面是几个类型标识,代表大数的存储类型,是用于下标变量和长度变量的。代表了位长是两倍的类型,做除法时会用到。

,是BBP公式四项的参数。

下面定义结构体来记录我们的大数,其中记录大整数最高位所在的位置,下标记录了自低位到高位。以及记录除法的除数。

struct BitSet {
    BASE_t d[ARRAY_SZ];
    ITER_t len;
} tmp[4], ans;

BASE_t divd[4];

我们的主要逻辑如下:

for (int k = 0; k < LIM; k++) {
        for (int i = 0; i < 4; i++) {
            divd[i] = (BASE_t) k << 3 | var2[i];
            set_bit(tmp + i, (ITER_t) (4 * (LIM - k) + var1[i]));
            divide(tmp + i, divd[i]);
            if (i)
                dec(tmp + i);
            else
                add(tmp + i);
        }
    }

外层循环BBP公式的项,内层循环代表一项的四个部分。函数为设置成分母(某一位是,其余位都是),接着做除法,最后是加法或者减法,将的答案更新到中。

下面是重要的四个函数的实现:

void set_bit(struct BitSet *x, ITER_t pos) {
    memset(x->d, 0, sizeof(BASE_t) * (x->len + 1));
    x->len = pos / SZ_BASE_t;
    x->d[x->len] = (BASE_t) 1 << pos % SZ_BASE_t;
}

void divide(struct BitSet *x, BASE_t val) {
    BASE_t c = 0;
    DOUBLE_BASE_t temp;
    for (ITER_t i = x->len; i != ITER_END; i--) {
        temp = x->d[i] + ((DOUBLE_BASE_t) c << SZ_BASE_t);
        x->d[i] = (BASE_t) (temp / val);
        c = (BASE_t) (temp % val);
    }
    if (x->len >= 0 && !x->d[x->len])
        --x->len;
}

void add(struct BitSet *y) {
    if (y->len == ITER_END)
        return;
    BASE_t c = 0, t;
    ITER_t l = y->len;
    if (!ans.len)
        ans.len = l;
    ans.d[ans.len + 1] = 0;
    for (ITER_t i = 0; i <= l; i++) {
        t = c;
        c = (BASE_t) ((y->d[i] == BASE_INF && c) || BASE_INF - y->d[i] - c < ans.d[i]);
        ans.d[i] += y->d[i] + t;
    }
    while (c) {
        ++l;
        if (ans.d[l] != BASE_INF)
            ++ans.d[l], c = 0;
        else
            ans.d[l] = 0;
    }
}

void dec(struct BitSet *y) {
    if (y->len == ITER_END)
        return;
    BASE_t c = 0, t;
    ITER_t l = y->len;
    for (ITER_t i = 0; i <= l; i++) {
        t = c;
        c = (BASE_t) ((y->d[i] == BASE_INF && c) || ans.d[i] < y->d[i] + c);
        ans.d[i] -= y->d[i] + t;
    }
    while (c) {
        ++l;
        if (!ans.d[l])
            ans.d[l] = BASE_INF;
        else
            --ans.d[l], c = 0;
    }
}

这里不作过多解释。需要注意的是,这部分代码利用了一些鲁棒性不是很好的事实:,一开始是全部变量,初始为,以及除了首次加法,之后的加减法不会出现进位和退位等等。我先实现完整的代码再做删减,这样可以尽量保证不会出错误,效率也会好些。

并行化

最近刚刚接触了基础openmp,这里可以给这个程序做个小小的并行化改动。

我们将每一项,当做一个任务,那么一共有个任务,可以分给本机的四线程。另外,由于对于的修改不应冲突,所有用子句加以限制。另外要注意,线程做了一次加法之后,一旦,,都要做减法,那么可能导致出现暂时的减出负数的情况。这不是我们希望看到的,因此,我们需要把第一项单独拎出来先做。

实现时发现一个有趣的事情。并行化之前和并行化之后,我用中的函数计时,结果之后反而变慢了一点。然而感官感觉时间缩短了不少。经过了解之后才知道统计的是运行时,而非程序运行时间。这里需要用中的来计时。


总代码如下

#include 
#include 
#include 

#define LIM 100008
#define ARRAY_SZ 12505  //LIM*4/SZ_BASE_t+5

typedef unsigned int BASE_t;
typedef unsigned int ITER_t;
typedef unsigned long long DOUBLE_BASE_t;

const ITER_t SZ_BASE_t = sizeof(BASE_t) * 8;
const ITER_t ITER_END = (ITER_t) (-1);
const BASE_t var1[4] = {2, 1, 0, 0};
const BASE_t var2[4] = {1, 4, 5, 6};
const BASE_t BASE_INF = (BASE_t) (-1);

struct BitSet {
    BASE_t d[ARRAY_SZ];
    ITER_t len;
} tmp[4], ans;

BASE_t divd[4];

void set_bit(struct BitSet *x, ITER_t pos) {
    memset(x->d, 0, sizeof(BASE_t) * (x->len + 1));
    x->len = pos / SZ_BASE_t;
    x->d[x->len] = (BASE_t) 1 << pos % SZ_BASE_t;
}

void divide(struct BitSet *x, BASE_t val) {
    BASE_t c = 0;
    DOUBLE_BASE_t temp;
    for (ITER_t i = x->len; i != ITER_END; i--) {
        temp = x->d[i] + ((DOUBLE_BASE_t) c << SZ_BASE_t);
        x->d[i] = (BASE_t) (temp / val);
        c = (BASE_t) (temp % val);
    }
    if (x->len >= 0 && !x->d[x->len])
        --x->len;
}

void add(struct BitSet *y) {
    if (y->len == ITER_END)
        return;
    BASE_t c = 0, t;
    ITER_t l = y->len;
    if (!ans.len)
        ans.len = l;
    ans.d[ans.len + 1] = 0;
    for (ITER_t i = 0; i <= l; i++) {
        t = c;
        c = (BASE_t) ((y->d[i] == BASE_INF && c) || BASE_INF - y->d[i] - c < ans.d[i]);
        ans.d[i] += y->d[i] + t;
    }
    while (c) {
        ++l;
        if (ans.d[l] != BASE_INF)
            ++ans.d[l], c = 0;
        else
            ans.d[l] = 0;
    }
}

void dec(struct BitSet *y) {
    if (y->len == ITER_END)
        return;
    BASE_t c = 0, t;
    ITER_t l = y->len;
    for (ITER_t i = 0; i <= l; i++) {
        t = c;
        c = (BASE_t) ((y->d[i] == BASE_INF && c) || ans.d[i] < y->d[i] + c);
        ans.d[i] -= y->d[i] + t;
    }
    while (c) {
        ++l;
        if (!ans.d[l])
            ans.d[l] = BASE_INF;
        else
            --ans.d[l], c = 0;
    }
}

int main() {
    FILE *fo = fopen("/home/max/桌面/1.txt", "w");
    double sta = omp_get_wtime();
    for (int i = 0; i < 4; i++) {
        divd[i] = (BASE_t) var2[i];
        set_bit(tmp + i, (ITER_t) (4 * LIM + var1[i]));
        divide(tmp + i, divd[i]);
        if (i)
            dec(tmp + i);
        else
            add(tmp + i);
    }
#pragma omp parallel for firstprivate(tmp, divd)
    for (int k = 1; k < LIM; k++) {
        for (int i = 0; i < 4; i++) {
            divd[i] = (BASE_t) k << 3 | var2[i];
            set_bit(tmp + i, (ITER_t) (4 * (LIM - k) + var1[i]));
            divide(tmp + i, divd[i]);
#pragma omp critical(the_ans)
            if (i)
                dec(tmp + i);
            else
                add(tmp + i);
        }
    }
    printf("%.10lf\n", omp_get_wtime() - sta);
    fprintf(fo, "%x", ans.d[ans.len]);
    for (ITER_t i = ans.len - 1; i; i--)
        fprintf(fo, "%08x", ans.d[i]);
    return 0;
}

测试效果

CPU 酷睿i5双核四线程,运行环境为Clion,编译器为gcc 7.5.0,Release模式,大致相当于gcc -fopenmp -O3 -o main.out main.c

并行前效率为左右,胜过Python。其中占用比较多的是函数,占用大概十分之九的时间。原因在于取模和除法运算都比较耗时。

并行后在到之间,提升大约一倍。

将基础类型换成,用时会略有增长。换成,会出现分母溢出的情况(较大时大于),减小再观察,逊于前两者。因此位机子,还是用比较合适。

一些说明和潜在的优化

  1. 并行并不是很彻底。这里计算机分配任务可能不均匀。另外虽然用时只占有十分之一,但是下造成的阻塞,估计也会增长时间左右。可以有其他的并行方法,另外也可以最后进行答案的聚合,避开。
  2. 除法和取模是同一种运算,或许可以用汇编优化,同时得到商和余数。
  3. 这里只是求出了十六进制。如果要转换成十进制,需要乘以除以,再从十六进制转换为十进制,过程麻烦,而且效率是的。十六进制转换为十进制暂时想不出非常高效率且方便的做法。留给大家思考好了。

你可能感兴趣的:(BBP公式计算十万位十六进制圆周率(C语言))