无心人 发表于 2019-11-3 10:15:44

刚反复的测试,觉得结果推进到3.1415926535897,需要的精度是1.388E-14
所以,每次误差不能超过5E-15
我无论怎么测试,gcc每次浮点误差都大于1E-17,所以斯特拉公式调整频率得降低到100次1调整才行

无心人 发表于 2019-11-3 10:27:49

无心人 发表于 2019-11-3 10:15
刚反复的测试,觉得结果推进到3.1415926535897,需要的精度是1.388E-14
所以,每次误差不能超过5E-15
我 ...

找到原因了,把double改long double, log10改log10l, 相应的输入输出格式化字符串%lf改%Lf
编译参数加-m128bit-long-double
后,测试精度高于5E-19,已经满足1000次修正后,计算到3.1415926535897精度大于1E-14的要求

现在重新复核上次计算到22.8万亿的过程

Ickiverar 发表于 2019-12-11 00:12:04

如果我们写一个十进制定点数的乘法算法,只保留十进制前几十位数,使得从1乘到很大的N都不会产生严重误差,是不是可以避免浮点斯特令公式?

Ickiverar 发表于 2019-12-11 02:27:39

我写了一个算法,可以单线程在18秒内搜索10亿的范围(测试起点 0xdeadbeefbadcab1e , 测试长度 10^9 ),可以任意并行化,最高可以搜索到 2^64 - 2 的阶乘。
但我的电脑正在全线程计算海绵球的体积,所以这个项目得过几天开算。

Ickiverar 发表于 2019-12-11 02:33:20

我先把我的代码贴出来吧。
使用到了mpfrc++,可以在 http://www.holoborodko.com/pavel/mpfr/ 下载。
用64位无符号定点数存储阶乘的十进制的开头,具体算法见 einit。
然后用emul逐项乘下一个数进行递推(可以每递推百万次用einit校正一次)。
搜索比较接近 pi/10*2^64 的值就行了。

#include<mpreal.h>
#pragma comment(lib,"mpfr-4.0.1.lib")
#include<iostream>
#include<intrin.h>

typedef mpfr::mpreal real;

//10^frac(log10(factorial(n)))*2^64/10
uint64_t einit(uint64_t n){
    static const real log10e=1/mpfr::log(10);
    static const real p264=mpfr::pow(2,64);
    return (uint64_t)(p264*mpfr::pow(10,mpfr::frac(mpfr::lngamma(n+1)*log10e)-1));
}

//2^64/10 ~< e*n/10^k < 2^64
uint64_t emul(uint64_t e,uint64_t n){
    //10^pos
    static uint64_t pow10[]={
      1,10,100,1000,10000,
      100000,1000000,10000000,100000000,1000000000,
      10000000000,100000000000,1000000000000,10000000000000,100000000000000,
      1000000000000000,10000000000000000,100000000000000000,1000000000000000000,10000000000000000000
    };
    static int shrn[]={3,6,9,13,16,19,23,26,29,33,36,39,43,46,49,53,56,59,63,66};
    //round(2^(64+shrn)/10^(pos+1))
    static uint64_t inv10[]={
      0xcccccccccccccccd,0xa3d70a3d70a3d70a,0x83126e978d4fdf3b,0xd1b71758e219652c,
      0xa7c5ac471b478423,0x8637bd05af6c69b6,0xd6bf94d5e57a42bc,0xabcc77118461cefd,
      0x89705f4136b4a597,0xdbe6fecebdedd5bf,0xafebff0bcb24aaff,0x8cbccc096f5088cc,
      0xe12e13424bb40e13,0xb424dc35095cd80f,0x901d7cf73ab0acd9,0xe69594bec44de15b,
      0xb877aa3236a4b449,0x9392ee8e921d5d07,0xec1e4a7db69561a5,0xbce5086492111aeb
    };

    //res = e*n
    uint64_t res;
    res=_umul128(e,n,res+1);

    if(res==0)return res;

    //shrt = res >> shrn
    int pos=0;
    uint64_t shrt;
    if(res>=pow10){
      pos=19;
      shrt=res>>2;
      res=0;
    }
    else{
      while(res>=pow10)++pos;
      shrt=res<<64-shrn|res>>shrn;
      res=res>>shrn?inv10:0;
    }
   
    _umul128(shrt,inv10,res+1);
    return res+res;
}

int main(){
    mpfr::mpreal::set_default_prec(192);
    std::cout.precision(39);

    uint64_t iinit=0xdeadbeefbadcab1e,isize=1000000000;
    uint64_t eval=einit(iinit);
    for(uint64_t i=iinit+1;i<=iinit+isize;++i){
      eval=emul(eval,i);
    }
    std::cout<<eval<<std::endl;
    std::cout<<einit(iinit+isize)<<std::endl;
    return 0;
}


mathe 发表于 2019-12-11 06:27:36

最关键是误差分析,乘百万次再调整精度应该不够了

liangbch 发表于 2019-12-11 09:38:55

Mark, 以后有时间看

Ickiverar 发表于 2019-12-11 16:44:41

本帖最后由 Ickiverar 于 2019-12-11 18:18 编辑

2019.12.11 16:42,在我的老电脑上四线程并行开算。
16秒40亿,一天应该能算21万亿。
更新:
18:18算出前12个数,验证了20楼的结果:
d =0, n = 9
d =1, n = 62
d =2, n = 62
d =3, n = 10044
d =4, n = 50583
d =5, n = 1490717
d =6, n = 5573998
d =7, n = 65630447
d =8, n = 688395641
d =9, n = 5777940569
d = 10, n = 77773146302
d = 11, n = 1154318938997
d = 12, n = 1544607046599
done to n =      1552000000000, maxerr = 0.109788

liangbch 发表于 2019-12-13 15:03:58

我尝试了把105#的代码修改为可在64位Linux下可用gcc编译通过的代码。
1. 在GCC下,不需要这句 “#pragma comment(lib,"mpfr-4.0.1.lib")”
2. 在GCC下,没有"intrin.h"这个文件,所以也就没有_umul128这个函数。
3. 在GCC下,10000000000000000000要改为 10000000000000000000ULL

修改思路,
首先可实现 c语言版的_umul128。修改后的部分代码如下。
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <iostream>
#include <mpreal.h>

typedef unsigned __int128uint128_t;

uint64_t _umul128( uint64_t x,uint64_t y,uint64_t *HighPart)
{
    uint128_t __res = (uint128_t)x * y;
    *HighPart = (uint64_t) (__res >> 64);
   return (uint64_t) __res;
}


代码通译通过后,可考虑改为汇编语言的实现。
假设上面的包含 _umul128的源代码编译为 test.o(目标文件,非可执行文件),执行下面的命令可看到这个函数汇编后的代码。
objdump -d test.o -M intel

稍加修改,改为nasm格式的汇编文件,保存为umul128.asm
global _umul128

section .text
_umul128:
   mov    rax,rdi
   mov    rcx,rdx
   mul    rsi
   mov    QWORD ,rdx
   ret


3. 将_umul128的c语言的实现删除,改为声明
extern "C" uint64_t _umul128( uint64_t x,uint64_t y,uint64_t *HighPart);

4. 现在,就可以使用汇编版的_umul128了,下面的编译链接命令
g++ -m64 -c test.cpp -O2 -o test.o
nasm -f elf64 -o umul128.o umul128.asm
g++ test.o umul128.o -lmpfr -o test

liangbch 发表于 2019-12-13 15:38:39

liangbch 发表于 2019-12-13 15:03
我尝试了把105#的代码修改为可在64位Linux下可用gcc编译通过的代码。
1. 在GCC下,不需要这句 “#pragma c ...

从性能上将,c语言的版本并不比汇编版本差。不过,将此函数替换为nasm格式的汇编代码,使得代码在跨平台(windows/Liunx)方面,更有优势,因为nasm本身就是跨平台的。
页: 1 2 3 4 5 6 7 8 9 10 [11] 12 13 14
查看完整版本: 阶乘和圆周率