BBP公式简介
贝利-波尔温-普劳夫公式(BBP 公式) 提供了一个计算圆周率π的第n位二进制数的算法。于1985年被提出。其衍生出的贝拉公式,是当前世界上计算机实现圆周率计算效率最高的公式之一。公式的形式如下。
公式的形式似乎不是很优美。但是摁一下计算器就可以发现,收敛的速度是很快的。注意到式子中很适合计算机二进制,这也是我们快速求圆周率十六进制表示的关键。
为了求前面位十六进制的圆周率,首先可以将式子两头乘以,得到:
左边式子可以看做的十六进制表示下,小数点向右侧位移了次,整数部分,就是我们想要的答案。
可以看出,右边式子前面的项对于前面位有决定性意义。而后面一项则有影响,但是影响不大。
如果将第一个求和项中的除法全部换成向下的整除,并且省略掉第二个求和项。会对末尾的几位造成较大影响。因此一般会选择将实际取的增大一些,例如我们今天要求,稍微安全点就应该取,多求八位,还不保险就取更大一些,然后得到答案时就取前面即可。
剩下的第一个求和项,实现起来就并不困难了。
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。其中占用比较多的是函数,占用大概十分之九的时间。原因在于取模和除法运算都比较耗时。
并行后在到之间,提升大约一倍。
将基础类型换成,用时会略有增长。换成,会出现分母溢出的情况(较大时大于),减小再观察,逊于前两者。因此位机子,还是用比较合适。
一些说明和潜在的优化
- 并行并不是很彻底。这里计算机分配任务可能不均匀。另外虽然用时只占有十分之一,但是下造成的阻塞,估计也会增长时间左右。可以有其他的并行方法,另外也可以最后进行答案的聚合,避开。
- 除法和取模是同一种运算,或许可以用汇编优化,同时得到商和余数。
- 这里只是求出了十六进制。如果要转换成十进制,需要乘以除以,再从十六进制转换为十进制,过程麻烦,而且效率是的。十六进制转换为十进制暂时想不出非常高效率且方便的做法。留给大家思考好了。