超越.NET极限:我打造的高精度数值计算库
当前位置:点晴教程→知识管理交流
→『 技术文档交流 』
还记得那一天,我大学刚毕业,紧张又兴奋地走进人生第一场
就这样,这场面试,像一颗种子,在我心里种下了对高精度数值计算的追求。我知道大家都会说,谁还不会算个数呢?但其实,还真有很多需要高精度数值计算的情况。 比如你正在计算一个飞向火星的火箭的轨道,一个微小的计算误差,可能就会让火箭偏离数百万公里。或者你正在使用GPS导航,在城市的密集街道中,几十米的误差就可能让你迷路。又或者你是一个气候科学家,正在预测全球变暖的趋势,一点点的计算误差,就可能导致模型的预测结果大相径庭。 从那个时候起,我开始关注一些技术问题的本质,而不是仅仅将需求解决。我也很想知道 但随着时间的推移,我逐渐积累了更多的知识和技能。为实现当年的梦想,今年(2023)年初趁过年放假期间,我把自己关在家里,连续几个晚上熬夜工作,基于 市面上已有的GMP和MPFR封装库及其问题在打造我的 首先,让我们看看 接下来,我们看看 此外,上述两个项目都存在一个共同的问题,那就是它们的版本都过于陈旧,且很久没有得到维护和更新。在快速变化的技术世界中,一个库如果连续数年没有更新,那么它可能会失去与最新技术接轨的机会,而这对于用户来说是无法接受的。 当然,除了以上的原因外,还有一个更重要的原因驱使我创造新的库,那就是我想要做出一个更好用的 NuGet包简介这个项目分为两部分,
如果你熟悉我的另一个项目 对于
最后,所有的 值得一提的是, libgmpmpfr使用示例 - 计算2^65536的最后20位数字在这个章节中,我们首先展示如何使用 原生
|
| 实现方式 | 平均耗时(ms) | 结果 |
|---|---|---|
原生.NET实现 |
94.00 | 45587895905719156736 |
无优化的Sdcb.Arithmetic |
89.52 | 45587895905719156736 |
使用C API优化的Sdcb.Arithmetic |
20.87 | 45587895905719156736 |
使用InplaceAPI优化的Sdcb.Arithmetic |
21.13 | 45587895905719156736 |
Java - BigInteger |
103.10 | 45587895905719156736 |
在这个示例中,我将展示Sdcb.Arithmetic.Gmp计算小数点后100万位π的计算方式,这段代码著名的Chudnovsky算法来计算圆周率π的值,该算法由Chudnovsky兄弟在1980年代提出的,它基于Ramanujan的公式,但在计算精度和效率上进行了改进。这个算法的一个显著特点是它的超级线性收敛性,即每增加一个迭代步骤,就可以大幅增加精确到的小数位数。实际上,Chudnovsky算法每次迭代大约能增加14个准确的小数位数。这使得它非常适合计算几百万位甚至更高的π值。
// Install NuGet package: Sdcb.Arithmetic.Gmp
// Install NuGet package: Sdcb.Arithmetic.Gmp.runtime.win-x64(for windows)
using Sdcb.Arithmetic.Gmp;
Stopwatch sw = Stopwatch.StartNew();
using GmpFloat pi = CalcPI();
double elapsed = sw.Elapsed.TotalMilliseconds;
Console.WriteLine($"耗时:{elapsed:F2}ms");
Console.WriteLine($"结果:{pi:N1000000}");
GmpFloat CalcPI(int inputDigits = 1_000_000)
{
const double DIGITS_PER_TERM = 14.1816474627254776555; // = log(53360^3) / log(10)
int DIGITS = (int)Math.Max(inputDigits, Math.Ceiling(DIGITS_PER_TERM));
uint PREC = (uint)(DIGITS * Math.Log2(10));
int N = (int)(DIGITS / DIGITS_PER_TERM);
const int A = 13591409;
const int B = 545140134;
const int C = 640320;
const int D = 426880;
const int E = 10005;
const double E3_24 = (double)C * C * C / 24;
using PQT pqt = ComputePQT(0, N);
GmpFloat pi = new(precision: PREC);
// pi = D * sqrt((mpf_class)E) * PQT.Q;
pi.Assign(GmpFloat.From(D, PREC) * GmpFloat.Sqrt((GmpFloat)E, PREC) * (GmpFloat)pqt.Q);
// pi /= (A * PQT.Q + PQT.T);
GmpFloat.DivideInplace(pi, pi, GmpFloat.From(A * pqt.Q + pqt.T, PREC));
return pi;
PQT ComputePQT(int n1, int n2)
{
int m;
if (n1 + 1 == n2)
{
PQT res = new()
{
P = GmpInteger.From(2 * n2 - 1)
};
GmpInteger.MultiplyInplace(res.P, res.P, 6 * n2 - 1);
GmpInteger.MultiplyInplace(res.P, res.P, 6 * n2 - 5);
GmpInteger q = GmpInteger.From(E3_24);
GmpInteger.MultiplyInplace(q, q, n2);
GmpInteger.MultiplyInplace(q, q, n2);
GmpInteger.MultiplyInplace(q, q, n2);
res.Q = q;
GmpInteger t = GmpInteger.From(B);
GmpInteger.MultiplyInplace(t, t, n2);
GmpInteger.AddInplace(t, t, A);
GmpInteger.MultiplyInplace(t, t, res.P);
// res.T = (A + B * n2) * res.P;
if ((n2 & 1) == 1) GmpInteger.NegateInplace(t, t);
res.T = t;
return res;
}
else
{
m = (n1 + n2) / 2;
PQT res1 = ComputePQT(n1, m);
using PQT res2 = ComputePQT(m, n2);
GmpInteger p = res1.P * res2.P;
GmpInteger q = res1.Q * res2.Q;
// t = res1.T * res2.Q + res1.P * res2.T
GmpInteger.MultiplyInplace(res1.T, res1.T, res2.Q);
GmpInteger.MultiplyInplace(res1.P, res1.P, res2.T);
GmpInteger.AddInplace(res1.T, res1.T, res1.P);
res1.P.Dispose();
res1.Q.Dispose();
return new PQT
{
P = p,
Q = q,
T = res1.T,
};
}
}
}
public ref struct PQT
{
public GmpInteger P;
public GmpInteger Q;
public GmpInteger T;
public readonly void Dispose()
{
P?.Dispose();
Q?.Dispose();
T?.Dispose();
}
}
在我的i9-9880h电脑中,输出如下(100万位中间有...省略):
耗时:435.35ms
结果:3.141592653589793238462643383...83996346460422090106105779458151
可见速度是非常快的,100万位π的值可以在这个链接进行参考。
同时也作为比较,我也参考了Github上网友写的这段C++计算100万位π的代码:https://gist.github.com/komasaru/68f209118edbac0700da
我使用VS2022通过Release-x64编译,在同一台电脑中,输出如下:
**** PI Computation ( 1000000 digits )
TIME (COMPUTE): 0.425 seconds.
TIME (WRITE) : 0.103 seconds.
我也参考了Github上另一段用C写的同样计算100万位π的代码:https://github.com/natmchugh/pi/blob/master/gmp-chudnovsky.c
我同样使用VS2022通过Release-x64编译,输出如下:
#terms=70513, depth=18
sieve time = 0.003
..................................................
bs time = 0.265
gcd time = 0.000
div time = 0.037
sqrt time = 0.022
mul time = 0.014
total time = 0.343
P size=1455608 digits (1.455608)
Q size=1455601 digits (1.455601)
这是3种编程语言计算100万位π耗时的比较表格:
| 耗时(ms) | |
|---|---|
| C# | 435.35 |
| C++ | 425 |
| C | 343 |
可见C#和C++几乎不相上下(在我的本地测试中甚至有时C#更快),使用C确实稍快,但从代码可以看出C的实现方式有许多优化,比如递归求值部分的内存是一次性分配的,速度快的主要原因是算法有优化。
在这篇文章中,我分享了我从大学毕业的第一场面试中获得的启示,以及我如何把这种启示转化为实践,打造了一个.NET的高精度数值计算库——Sdcb.Arithmetic。这个开源项目弥补了C#在处理大数运算方面的不足,打破了Java的BigFloat优势,使得C#也能轻松处理高精度计算的需求。
这个库包括GMP和MPFR两部分,前者支持高精度的整数、小数和分数的运算,后者则是处理高精度小数的利器,提供了超过300个MPFR库函数。通过我对这个库的介绍和展示,你可以看到这个库在处理大数计算,例如计算2^65536的最后20位数字,或者计算π的百万位小数上的表现非常出色。
阅读原文:点击这里