首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >高精度.NET数值计算库Sdcb.Arithmetic全平台发布

高精度.NET数值计算库Sdcb.Arithmetic全平台发布

作者头像
郑子铭
发布2025-08-09 13:38:15
发布2025-08-09 13:38:15
6600
代码可运行
举报
运行总次数:0
代码可运行

高精度.NET数值计算库Sdcb.Arithmetic全平台发布

这其实是一个迟来的好消息——今年春节期间我完成了一项重要工作却未大规模宣传,现在终于可以和大家分享:「Sdcb.Arithmetic库已实现全平台发布!」

这是一个基于GMP(GNU Multiple Precision Arithmetic Library)和MPFR(GNU MPFR Library)的高精度数值计算库,旨在为.NET开发者提供「跨平台、高效、精确、开箱即用」的数值计算解决方案。它支持无限精度的整数和浮点数运算,适用于科学计算、金融建模等需要高精度数值处理的场景。无论是计算100万位的圆周率π,还是模拟复杂的天体运动轨迹,都能轻松应对。

虽然对多数开发者而言这可能是个小众领域,但对我而言却是执念的结晶。从高中参加NOIP到大学征战ACM竞赛,高精度数值计算始终伴随左右。那时我们有个共识:遇到高精度计算题就选Java,因其内置的BigInteger/BigDecimal类能轻松处理大数运算。反观.NET平台,虽然.NET Framework 4.0加入了BigInteger,却始终缺失高性能的BigFloat实现。因此我立下目标:要以最高标准打造.NET的高精度数值计算库(当然,目前仍有改进空间)。

该项目实际在2023年就已发布:超越.NET极限-我做的高精度数值计算库,但初期仅支持Windows平台。今年春节前,受开源项目pdfium-binaries启发,我决心将跨平台方案应用到自己的开源项目中。经过整个春节假期的全力攻坚,终于实现了全平台支持的目标。 📦 多平台NuGet包发布 libgmp运行时包

Package Id

Version

License

Sdcb.Arithmetic.Gmp

MIT

Sdcb.Arithmetic.Gmp.runtime.win-x64

LGPL

Sdcb.Arithmetic.Gmp.runtime.win-x86

LGPL

Sdcb.Arithmetic.Gmp.runtime.linux-x64

LGPL

Sdcb.Arithmetic.Gmp.runtime.linux-x86

LGPL

Sdcb.Arithmetic.Gmp.runtime.linux-arm

LGPL

Sdcb.Arithmetic.Gmp.runtime.linux-arm64

LGPL

Sdcb.Arithmetic.Gmp.runtime.linux-musl-x64

LGPL

Sdcb.Arithmetic.Gmp.runtime.linux-musl-arm64

LGPL

Sdcb.Arithmetic.Gmp.runtime.osx-arm64

LGPL

Sdcb.Arithmetic.Gmp.runtime.osx-x64

LGPL

Sdcb.Arithmetic.Gmp.runtime.android-arm

LGPL

Sdcb.Arithmetic.Gmp.runtime.android-arm64

LGPL

Sdcb.Arithmetic.Gmp.runtime.android-x86

LGPL

Sdcb.Arithmetic.Gmp.runtime.android-x64

LGPL

mpfr运行时包

Package Id

Version

License

Sdcb.Arithmetic.Mpfr

MIT

Sdcb.Arithmetic.Mpfr.runtime.win-x64

LGPL

Sdcb.Arithmetic.Mpfr.runtime.win-x86

LGPL

Sdcb.Arithmetic.Mpfr.runtime.linux-x64

LGPL

Sdcb.Arithmetic.Mpfr.runtime.linux-x86

LGPL

Sdcb.Arithmetic.Mpfr.runtime.linux-arm

LGPL

Sdcb.Arithmetic.Mpfr.runtime.linux-arm64

LGPL

Sdcb.Arithmetic.Mpfr.runtime.linux-musl-x64

LGPL

Sdcb.Arithmetic.Mpfr.runtime.linux-musl-arm64

LGPL

Sdcb.Arithmetic.Mpfr.runtime.osx-arm64

LGPL

Sdcb.Arithmetic.Mpfr.runtime.osx-x64

LGPL

Sdcb.Arithmetic.Mpfr.runtime.android-arm

LGPL

Sdcb.Arithmetic.Mpfr.runtime.android-arm64

LGPL

Sdcb.Arithmetic.Mpfr.runtime.android-x86

LGPL

Sdcb.Arithmetic.Mpfr.runtime.android-x64

LGPL

❝✅ 「关键进展」: 现已覆盖Windows、Linux、macOS、Android等主流平台,所有运行时包均通过全平台测试验证。 ℹ️ 注:iOS/WASM平台包已编译但暂未发布,因缺乏测试环境🚄 这个库怎么使用?

许多程序员可能已经熟悉.NET Framework 3.5引入的大整数类System.Numeric.BigInteger。我们可以使用这个类来计算2^65536的最后20位数字,代码如下:

代码语言:javascript
代码运行次数:0
运行
复制
Stopwatch sw = Stopwatch.StartNew();
int count = 10;

BigInteger b = new BigInteger();
for (int c = 0; c < count; ++c)
{
 b = 1;
 for (int i = 1; i <= 65536; ++i)
 {
  b *= 2;
 }
}
Console.WriteLine($"耗时:{sw.Elapsed.TotalMilliseconds / count:F2}ms");
Console.WriteLine($"2^65536最后20位数字:{b.ToString()[^20..]}");

在我的i9-9880h电脑上,这个程序的运行结果如下:

代码语言:javascript
代码运行次数:0
运行
复制
耗时:94.00ms
2^65536最后20位数字:45587895905719156736

使用Sdcb.Arithmetic

下面我们将展示如何使用我们的库Sdcb.Arithmetic来进行同样的计算。为了安装这个库,你需要使用以下的NuGet包:Sdcb.Arithmetic.GmpSdcb.Arithmetic.Gmp.runtime.win64(或其它对应环境包)。

代码语言:javascript
代码运行次数:0
运行
复制
Stopwatch sw = Stopwatch.StartNew();
int count = 10;

GmpInteger b = new GmpInteger();
for (int c = 0; c < count; ++c)
{
 b = 1;
for (int i = 1; i <= 65536; ++i)
 {
  b *= 2;
 }
}

Console.WriteLine($"耗时:{sw.Elapsed.TotalMilliseconds / count:F2}ms");
Console.WriteLine($"2^65536最后20位数字:{b.ToString()[^20..]}");

运行结果如下:

代码语言:javascript
代码运行次数:0
运行
复制
耗时:89.52ms
2^65536最后20位数字:45587895905719156736

可以看到,Sdcb.Arithmetic.Gmp的结果与.NET原生实现相匹配,并且计算速度相近。

性能优化

虽然上述Gmp代码已经达到了与原生.NET实现相近的性能,但是我们可以通过使用底层的C API来进一步优化我们的库。以下是优化后的代码:

代码语言:javascript
代码运行次数:0
运行
复制
// 安装NuGet包:Sdcb.Arithmetic.Gmp
// 安装NuGet包:Sdcb.Arithmetic.Gmp.runtime.win64 (或其它对应环境包)
// 函数需要标注unsafe
// 项目需要启用unsafe编译选项
Stopwatch sw = Stopwatch.StartNew();
int count = 10;
Mpz_t mpz;
GmpLib.__gmpz_init((IntPtr)(&mpz));

for (int c = 0; c < count; ++c)
{ 
 GmpLib.__gmpz_set_si((IntPtr)(&mpz), 1);
for (int i = 1; i <= 65536; ++i)
 {
  GmpLib.__gmpz_mul_si((IntPtr)(&mpz), (IntPtr)(&mpz), 2);
 }
}
sw.Stop();

IntPtr ret = GmpLib.__gmpz_get_str(IntPtr.Zero, 10, (IntPtr)(&mpz));
string wholeStr = Marshal.PtrToStringUTF8(ret)!;
GmpMemory.Free(ret);

Console.WriteLine($"耗时:{sw.Elapsed.TotalMilliseconds / count:F2}ms");
Console.WriteLine($"2^65536最后20位数字:{wholeStr[^20..]}");

GmpLib.__gmpz_clear((IntPtr)(&mpz));

在同一台电脑上,输出结果如下:

代码语言:javascript
代码运行次数:0
运行
复制
耗时:20.87ms
2^65536最后20位数字:45587895905719156736

速度易用两全其美 - 使用InplaceAPI优化

为了做到在性能和可维护性之间找到平衡,我还开发了InplaceAPI,这是一个直接调用底层API,但用起来很方便的函数。以下是使用该API后的代码:

代码语言:javascript
代码运行次数:0
运行
复制
// 安装NuGet包:Sdcb.Arithmetic.Gmp
// 安装NuGet包:Sdcb.Arithmetic.Gmp.runtime.win64 (或其它对应环境包)
Stopwatch sw = Stopwatch.StartNew();
int count = 10;

using GmpInteger b = new GmpInteger();
for (int c = 0; c < count; ++c)
{
 b.Assign(1);
for (int i = 1; i <= 65536; ++i)
 {
  GmpInteger.MultiplyInplace(b, b, 2);
 }
}

Console.WriteLine($"耗时:{sw.Elapsed.TotalMilliseconds / count:F2}ms");
Console.WriteLine($"2^65536最后20位数字:{b.ToString()[^20..]}");

对比Java

作为参考,当然少不了和Java的性能对比,这是用于对比的Java代码:

代码语言:javascript
代码运行次数:0
运行
复制
import java.math.BigInteger;
import java.time.Duration;
import java.time.Instant;

publicclass Main {
    public static void main(String[] args) {
        Instant start = Instant.now();

        int count = 10;
        BigInteger b = BigInteger.ONE;

        for (int c = 0; c < count; ++c) {
            b = BigInteger.ONE;
            for (int i = 1; i <= 65536; ++i) {
                b = b.multiply(BigInteger.valueOf(2));
            }
        }

        Instant finish = Instant.now();
        long timeElapsed = Duration.between(start, finish).toMillis();

        String str = b.toString();
        String last20Digits = str.substring(str.length() - 20);

        System.out.printf("耗时:%f ms\n", (double) timeElapsed / count);
        System.out.println("2^65536最后20位数字:" + last20Digits);
    }
}

我使用的Java版本是OpenJDK version "11.0.16.1" 2022-08-12 LTS,使用相同的电脑,输出结果如下:

代码语言:javascript
代码运行次数:0
运行
复制
耗时:103.100000 ms
2^65536最后20位数字:45587895905719156736

可见Java的速度比.NET原生的BigInteger稍慢,比Sdcb.Arithmetic.Gmp的速度慢了不少。

🚀 实战示例:百万位π计算

通过拉马努金公式计算100万位π值(完整算法见代码):

代码语言:javascript
代码运行次数:0
运行
复制
// 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)
{
    constdouble 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);
    constint A = 13591409;
    constint B = 545140134;
    constint C = 640320;
    constint D = 426880;
    constint E = 10005;
    constdouble 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();
            returnnew PQT
            {
                P = p,
                Q = q,
                T = res1.T,
            };
        }
    }
}

publicrefstruct PQT
{
    public GmpInteger P;
    public GmpInteger Q;
    public GmpInteger T;

    public readonly void Dispose()
    {
        P?.Dispose();
        Q?.Dispose();
        T?.Dispose();
    }
}

在我的i9-9880h电脑中,输出如下(100万位中间有...省略):

代码语言:javascript
代码运行次数:0
运行
复制
耗时:435.35ms
结果:3.141592653589793238462643383...83996346460422090106105779458151

可见速度是非常快的。

❝🔍 百万位π参考值 ❞


🔮 未来演进方向

这个项目离我的“执念”具体还有哪些差距呢?

1️⃣ 性能优化

虽然目前的性能已经很不错,但在某些极端情况下,可能还会有进一步的优化空间。 这个项目有一个很意外的事情,就是mpz/mpq/mpf这些内存现在其实是直接在class中分配的一个struct,为什么这样设计呢?因为我一开始使用了Marshal.AllocHGlobal来分配内存,但发现这样在绝大多数情况下性能反而更差,原因是因为Marshal.AllocHGlobal分配的内存是非托管内存,而在.NET中,直接new一个struct会从.NET的托管堆中分配内存,这样可以减少内存分配和释放的开销,尤其是在频繁创建和销毁对象的情况下。

但是代价是什么?代价就是每次调用PInvoke的时候都需要将这个struct pin起来,这样会导致性能下降,尤其是在频繁调用PInvoke的情况下——但是我测试了,这个性能下降远不如使用Marshal.AllocHGlobal分配内存的性能下降严重,因此我决定继续使用这种方式。

这是一些数据可供参考:

方案

初始化/释放操作数

PInvoke耗时

Struct in class

82,055,792 ops

1237ms

Raw memory IntPtr

15,543,619 ops

1134ms

可见,使用struct in class的方式在初始化和释放时性能远远好于使用Raw memory IntPtr的方式,速度快了5倍以上,在执行具体PInvoke计算时,性能下降了大约8%,我相信有了这个数据,大家就可以理解为什么我会选择使用struct in class的方式了。

有没有两全其美的办法呢?是有的,在这个项目发布之后我一直在思考这个问题,我想到了一个办法,就是使用一个内存池来管理这些struct,这样就可以减少内存分配和释放的开销,同时又可以避免频繁的pinning操作。这个想法我会在未来的版本中实现——或许需要等另一个春节。

2️⃣ 使用便利性

目前的API已经相对简单易用,但仍有提升空间,特别是如果你想兼顾性能的话,就不得不需要手动管理内存了(如上面的计算100万位π的例子,其中是优化过的)。

我觉得应该怎么做呢?我在想也许应该考虑加入一个Expression的机制,允许用户通过表达式来描述计算过程,这样可以在实际求值时进行优化里面的临时对象,从而减少内存分配和释放的开销。

3️⃣ .NET 7+特性支持

这个库目前还不支持.NET 7的Generic Math特性,这个特性可以让我们使用更通用的方式来处理数值计算,比如可以使用INumber<T>接口来定义数值类型的通用操作。

4️⃣ .NET 10 C# 14 用户定义的复合赋值运算符的支持

这个特性是我最最最想要的C#特性,在很久以前还在上大学的时候,我就写过一篇文章,那时候我说由于没有这个特性,就成为了C#的缺点之一。 没想到13年过去了,这个特性终于在C# 14中实现了!这个特性可以让我们定义自己的复合赋值运算符,比如+=-=等,这样可以让代码更简洁、更易读。 我一定会在未来的版本中加入这个特性,这样就可以让这个库的使用更加方便。

5️⃣数学求解器

这个库目前还没有加入求解器的功能,求解器可以让我们更方便地求解方程、积分等数学问题,也算是我的执念之一了,因为我对模拟天体运动很有兴趣,但现在求解器——比如龙格库塔法(Runge-Kutta)、欧拉法(Euler's method)、Cash-Karp法等等都需要用户手撸代码,而我希望能有一个现成的求解器可以直接使用,这样就可以更方便地进行数值计算 结语

Sdcb.Arithmetic的全平台发布标志着该库进入成熟阶段,现已覆盖Windows/Linux/macOS/Android等主流平台。无论您从事科学计算、金融分析还是高精度数值处理,本库都将成为您的得力工具。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2025-08-08,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 DotNet NB 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 高精度.NET数值计算库Sdcb.Arithmetic全平台发布
    • mpfr运行时包
    • 使用Sdcb.Arithmetic库
    • 性能优化
    • 速度易用两全其美 - 使用InplaceAPI优化
    • 对比Java
    • 🚀 实战示例:百万位π计算
    • 🔮 未来演进方向
      • 1️⃣ 性能优化
      • 2️⃣ 使用便利性
      • 3️⃣ .NET 7+特性支持
      • 4️⃣ .NET 10 C# 14 用户定义的复合赋值运算符的支持
      • 5️⃣数学求解器
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档