一夜孤行 發表於 2026-1-15 08:50:00

不服跑个分?.NET 10 大整数计算对阵 Java,结果令人意外

<h2 id="引言从经验值到无限大">引言:从“经验值”到无限大</h2>
<p>我对数值计算的执念,来自初中时代烟雾缭绕的网吧。那时玩《伝奇》,最让我着迷的不是打怪爆装备,而是角色面板里那条长长的<strong>经验值</strong>。看着数字不断跳动、累积,最终“叮”一声升级,那种简单的数值驱动整个世界运转的感觉,实在太奇妙了。</p>
<p>当时自学编程,从 G-BASIC 里只有 16 位的 <code>INTEGER</code>,到第一次发现 QBASIC <code>LONG</code> 能存下“20亿”时的兴奋,再到如今成为一名 .NET 程序员,手握理论上“无限大”的 <code>System.Numerics.BigInteger</code>。我对大数的迷恋从未改变,只是疑惑随之而来:</p>
<blockquote>
<p><strong>作为 .NET 开发者,我手里的 <code>BigInteger</code> 到底够不够快?特别是和隔壁 Java 的 <code>BigInteger</code> 相比,究竟谁更胜一筹?</strong></p>
</blockquote>
<p>尤其是涉及高精度计算、密码学等关键领域时,这不仅是好奇,更是关乎性能的严肃拷问。今天,我就通过一次详尽的对比测试(含 .NET 10、Java 21以及Java 8),来为大家揭晓答案。结果可能出乎意料,请务必看到最后,文末还有一个高性能的“彩蛋”。</p>
<hr>
<h2 id="实验目标与范围">实验目标与范围</h2>
<p>我们对比这三套实现:</p>
<ul>
<li><strong>.NET</strong>:<code>System.Numerics.BigInteger</code>(基于 .NET 10)</li>
<li><strong>Java</strong>:<code>java.math.BigInteger</code>
<ul>
<li><strong>Java 21(最新LTS)</strong>:代表未来趋势</li>
<li><strong>Java 8(国内存量最大)</strong>:代表庞大的现状</li>
</ul>
</li>
</ul>
<h3 id="测试操作3类覆盖常见大数热点">测试操作(3类,覆盖常见大数热点)</h3>
<ol>
<li><strong>ADD_MOD</strong>:<code>(a + b) mod m</code>(加法 + 取模)</li>
<li><strong>MUL_MOD</strong>:<code>(a * b) mod m</code>(乘法 + 取模)</li>
<li><strong>MODPOW</strong>:<code>a^e mod m</code>(模幂,密码学等场景的性能热点)</li>
</ol>
<h3 id="位宽3档">位宽(3档)</h3>
<ul>
<li>256 / 1024 / 4096 bits</li>
</ul>
<h3 id="计时口径尽量抑制噪声">计时口径(尽量抑制噪声)</h3>
<ul>
<li><strong>热身</strong>:预热 5 次,让 JIT(即时编译器)充分发挥。</li>
<li><strong>测量</strong>:正式跑 11 次,取中位数,避免单次抖动干扰。</li>
<li><strong>指标</strong>:<code>nsPerOp</code>(每次操作耗时多少纳秒),这个值<strong>越小越好</strong>。</li>
<li><strong>防作弊</strong>:用 <code>XOR</code> 聚合每次计算结果,防止聪明的编译器把整个循环优化掉。</li>
</ul>
<hr>
<h2 id="测试环境">测试环境</h2>
<blockquote>
<p>说明:以下是在同一 Linux 容器内完成。容器有资源限制,因此“看到的 CPU 核数/内存”与宿主机不完全一致。</p>
</blockquote>
<ul>
<li><strong>OS</strong>:Ubuntu 24.04.3 LTS</li>
<li><strong>CPU(宿主机型号)</strong>:AMD EPYC 7763 64-Core Processor<br>
<strong>容器可用核心数</strong>:2 核(受容器限制)</li>
<li><strong>内存</strong>:2GB(容器限制)</li>
<li><strong>.NET</strong>
<ul>
<li>SDK:10.0.101</li>
<li>Runtime:10.0.1</li>
<li>环境变量:<code>DOTNET_GCServer=1</code></li>
</ul>
</li>
<li><strong>Java</strong>
<ul>
<li>Java 21:OpenJDK 21.0.9(LTS)</li>
<li>Java 8:Temurin(OpenJDK) 1.8.0_472-b08(广泛使用的 8u 系列)</li>
<li>JVM 参数(两版本一致):
<ul>
<li><code>-Xms1g -Xmx1g</code>(把 Java 堆固定为 1GB,避免堆动态扩张干扰)</li>
<li><code>-XX:+UseG1GC</code></li>
<li><code>-XX:+AlwaysPreTouch</code>(尽量减少运行中页分配扰动)</li>
</ul>
</li>
</ul>
</li>
<li><strong>一致性约束</strong>
<ul>
<li>Java 8/Java 21 使用<strong>同一份源码</strong>(仅使用 Java 8 语法),并且 <strong>用 Java 8 的 <code>javac</code> 编译</strong>(classfile=52.0)后分别在 Java 8 / Java 21 上运行,从而尽量把差异归因到运行时/JIT/库实现,而不是编译器生成差异。</li>
</ul>
</li>
</ul>
<hr>
<h2 id="赛前插曲net-biginteger-真的不公平吗">赛前插曲:.NET BigInteger 真的“不公平”吗?</h2>
<p>有人可能想问:</p>
<blockquote>
<p>“.NET BigInteger 每次运算都会创建新的对象(不可变),不公平。”</p>
</blockquote>
<p>这句话在“大数场景”里基本成立,但<strong>Java BigInteger 同样是不可变类型</strong>:<code>add/multiply/mod/xor/modPow</code> 都会返回新值,业务代码层面你并没有公开的 in-place API 可以复用内部缓冲。</p>
<p>因此,在本文的“主对比”(.NET vs Java)里,双方都在不可变范式下运行,这点是公平的。</p>
<hr>
<h2 id="实验一net-biginteger-vs-java-21-biginteger">实验一:.NET BigInteger vs Java 21 BigInteger</h2>
<h3 id="31-完整源代码">3.1 完整源代码</h3>
<h4 id="java-源码bigintbenchjava">Java 源码:<code>BigIntBench.java</code></h4>
<blockquote>
<p>兼容 Java 8 语法;同一份代码在 Java 21 下运行。</p>
</blockquote>
<pre><code class="language-java">import java.math.BigInteger;
import java.util.*;

public class BigIntBench {
    // SplitMix64 RNG (deterministic, fast)
    static final class SplitMix64 {
      private long x;
      SplitMix64(long seed) { this.x = seed; }
      long nextLong() {
            long z = (x += 0x9E3779B97F4A7C15L);
            z = (z ^ (z &gt;&gt;&gt; 30)) * 0xBF58476D1CE4E5B9L;
            z = (z ^ (z &gt;&gt;&gt; 27)) * 0x94D049BB133111EBL;
            return z ^ (z &gt;&gt;&gt; 31);
      }
      void nextBytes(byte[] dst) {
            int i = 0;
            while (i &lt; dst.length) {
                long v = nextLong();
                for (int k = 0; k &lt; 8 &amp;&amp; i &lt; dst.length; k++) {
                  dst = (byte)(v &gt;&gt;&gt; (56 - 8*k)); // big-endian stream
                }
            }
      }
    }

    static BigInteger[] genBigInts(int bitSize, int count, long seed) {
      SplitMix64 rng = new SplitMix64(seed);
      int byteLen = (bitSize + 7) / 8;
      BigInteger[] arr = new BigInteger;
      byte[] buf = new byte;
      int topBit = (bitSize - 1) % 8;
      int keepBits = topBit + 1;
      int firstMask = (keepBits == 8) ? 0xFF : ((1 &lt;&lt; keepBits) - 1);
      byte topMask = (byte)(1 &lt;&lt; topBit);
      for (int i = 0; i &lt; count; i++) {
            rng.nextBytes(buf);
            // Ensure exact bit length:
            // - mask away unused top bits when bitSize is not byte-aligned
            // - set the top bit so the number has the requested bit length
            buf &amp;= (byte)firstMask;
            buf |= topMask;
            arr = new BigInteger(1, buf);
      }
      return arr;
    }

    static BigInteger genModulus(int bitSize, long seed) {
      SplitMix64 rng = new SplitMix64(seed);
      int byteLen = (bitSize + 7) / 8;
      byte[] buf = new byte;
      rng.nextBytes(buf);
      int topBit = (bitSize - 1) % 8;
      int keepBits = topBit + 1;
      int firstMask = (keepBits == 8) ? 0xFF : ((1 &lt;&lt; keepBits) - 1);
      buf &amp;= (byte)firstMask;
      buf |= (byte)(1 &lt;&lt; topBit);
      buf |= 1; // odd
      return new BigInteger(1, buf);
    }

    enum Op { ADD_MOD, MUL_MOD, MODPOW }

    static final class Result {
      final String lang;
      final int bits;
      final Op op;
      final long ops;
      final double nsPerOp;
      final long checksum;
      Result(String lang, int bits, Op op, long ops, double nsPerOp, long checksum) {
            this.lang = lang; this.bits = bits; this.op = op; this.ops = ops; this.nsPerOp = nsPerOp; this.checksum = checksum;
      }
      String toJson() {
            return String.format(Locale.ROOT,
                  "{\"lang\":\"%s\",\"bits\":%d,\"op\":\"%s\",\"ops\":%d,\"nsPerOp\":%.3f,\"checksum\":%d}",
                  lang, bits, op.name(), ops, nsPerOp, checksum);
      }
    }

    static long runOnce(Op op, BigInteger[] a, BigInteger[] b, BigInteger[] e, BigInteger mod, int outer) {
      BigInteger acc = BigInteger.ZERO;
      int n = a.length;
      switch (op) {
            case ADD_MOD:
                for (int o = 0; o &lt; outer; o++) {
                  for (int i = 0; i &lt; n; i++) {
                        BigInteger r = a.add(b).mod(mod);
                        acc = acc.xor(r);
                  }
                }
                break;

            case MUL_MOD:
                for (int o = 0; o &lt; outer; o++) {
                  for (int i = 0; i &lt; n; i++) {
                        BigInteger r = a.multiply(b).mod(mod);
                        acc = acc.xor(r);
                  }
                }
                break;

            case MODPOW:
                // here we use a.length as n; e can be same length
                for (int o = 0; o &lt; outer; o++) {
                  for (int i = 0; i &lt; n; i++) {
                        BigInteger r = a.modPow(e, mod);
                        acc = acc.xor(r);
                  }
                }
                break;

            default:
                throw new IllegalArgumentException("Unknown op: " + op);
      }
      return acc.longValue();
    }

    static Result bench(String lang, int bits, Op op, BigInteger[] a, BigInteger[] b, BigInteger[] e, BigInteger mod, long targetOps, int warmups, int measures) {
      int n = a.length;
      int outer = (int)Math.max(1, targetOps / n);
      long actualOps = (long)n * outer;

      // warmup
      long ck = 0;
      for (int i = 0; i &lt; warmups; i++) {
            ck ^= runOnce(op, a, b, e, mod, outer);
      }

      long[] times = new long;
      for (int i = 0; i &lt; measures; i++) {
            long t0 = System.nanoTime();
            long c = runOnce(op, a, b, e, mod, outer);
            long t1 = System.nanoTime();
            ck ^= c;
            times = (t1 - t0);
      }
      Arrays.sort(times);
      long median = times;
      double nsPerOp = (double)median / (double)actualOps;
      return new Result(lang, bits, op, actualOps, nsPerOp, ck);
    }

    static void printHuman(List&lt;Result&gt; results) {
      System.out.println("Java BigInteger benchmark");
      System.out.println("java.version=" + System.getProperty("java.version"));
      System.out.println("java.vm.name=" + System.getProperty("java.vm.name"));
      System.out.println();
      System.out.printf(Locale.ROOT, "%-6s %-9s %-12s %-12s\n", "Bits", "Op", "ns/op(med)", "ops/run");
      for (Result r : results) {
            System.out.printf(Locale.ROOT, "%-6d %-9s %-12.3f %-12d\n", r.bits, r.op.name(), r.nsPerOp, r.ops);
      }
      System.out.println();
      System.out.println("checksum=" + results.stream().mapToLong(x -&gt; x.checksum).reduce(0L, (x,y)-&gt;x^y));
    }

    public static void main(String[] args) {
      boolean json = false;
      for (String a : args) if (a.equals("--json")) json = true;

      int warmups = 5;
      int measures = 11;

      int[] bitSizes = new int[]{256, 1024, 4096};
      List&lt;Result&gt; results = new ArrayList&lt;&gt;();

      for (int bits : bitSizes) {
            BigInteger mod = genModulus(bits, 0xA1B2C3D4E5F60708L ^ bits);

            // add/mul datasets
            int nAddMul = 1024;
            BigInteger[] a = genBigInts(bits, nAddMul, 0x1111222233334444L ^ bits);
            BigInteger[] b = genBigInts(bits, nAddMul, 0x9999AAAABBBBCCCCL ^ bits);

            // modpow datasets (smaller)
            int nPow = 256;
            BigInteger[] ap = genBigInts(bits, nPow, 0x13579BDF2468ACE0L ^ bits);
            BigInteger[] ep = genBigInts(Math.min(bits, 512), nPow, 0x0FEDCBA987654321L ^ bits);

            long addOps;
            long mulOps;
            long powOps;
            if (bits == 256) {
                addOps = 2_000_000L;
                mulOps = 500_000L;
                powOps = 8_000L;
            } else if (bits == 1024) {
                addOps = 1_000_000L;
                mulOps = 120_000L;
                powOps = 1_500L;
            } else {
                addOps = 200_000L;
                mulOps = 20_000L;
                powOps = 250L;
            }

            results.add(bench("java", bits, Op.ADD_MOD, a, b, null, mod, addOps, warmups, measures));
            results.add(bench("java", bits, Op.MUL_MOD, a, b, null, mod, mulOps, warmups, measures));
            results.add(bench("java", bits, Op.MODPOW, ap, null, ep, mod, powOps, warmups, measures));
      }

      if (json) {
            for (Result r : results) System.out.println(r.toJson());
      } else {
            printHuman(results);
      }
    }
}
</code></pre>
<h4 id="net-源码programcsbiginteger-部分">.NET 源码:<code>Program.cs</code>(BigInteger 部分)</h4>
<blockquote>
<p>这是“主对比”用的 .NET 基准程序。</p>
</blockquote>
<pre><code class="language-csharp">using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Numerics;

sealed class SplitMix64
{
    private ulong _x;
    public SplitMix64(ulong seed) =&gt; _x = seed;

    public ulong NextUInt64()
    {
      ulong z = (_x += 0x9E3779B97F4A7C15UL);
      z = (z ^ (z &gt;&gt; 30)) * 0xBF58476D1CE4E5B9UL;
      z = (z ^ (z &gt;&gt; 27)) * 0x94D049BB133111EBUL;
      return z ^ (z &gt;&gt; 31);
    }

    public void NextBytes(byte[] dst)
    {
      int i = 0;
      while (i &lt; dst.Length)
      {
            ulong v = NextUInt64();
            for (int k = 0; k &lt; 8 &amp;&amp; i &lt; dst.Length; k++)
            {
                dst = (byte)(v &gt;&gt; (56 - 8 * k)); // big-endian stream
            }
      }
    }
}

enum Op { ADD_MOD, MUL_MOD, MODPOW }

record Result(string Lang, int Bits, Op Op, long Ops, double NsPerOp, long Checksum)
{
    public string ToJson() =&gt; string.Create(CultureInfo.InvariantCulture,
      $"{{\"lang\":\"{Lang}\",\"bits\":{Bits},\"op\":\"{Op}\",\"ops\":{Ops},\"nsPerOp\":{NsPerOp:F3},\"checksum\":{Checksum}}}");
}

static class BigIntBench
{
    static BigInteger[] GenBigInts(int bitSize, int count, ulong seed)
    {
      var rng = new SplitMix64(seed);
      int byteLen = (bitSize + 7) / 8;
      var arr = new BigInteger;
      var buf = new byte;
      int topBit = (bitSize - 1) % 8;
      byte topMask = (byte)(1 &lt;&lt; topBit);

      for (int i = 0; i &lt; count; i++)
      {
            rng.NextBytes(buf);
            buf |= topMask;
            // unsigned + big-endian prevents negative
            arr = new BigInteger(buf, isUnsigned: true, isBigEndian: true);
      }
      return arr;
    }

    static BigInteger GenModulus(int bitSize, ulong seed)
    {
      var rng = new SplitMix64(seed);
      int byteLen = (bitSize + 7) / 8;
      var buf = new byte;
      rng.NextBytes(buf);
      int topBit = (bitSize - 1) % 8;
      buf |= (byte)(1 &lt;&lt; topBit);
      buf[^1] |= 1; // odd
      return new BigInteger(buf, isUnsigned: true, isBigEndian: true);
    }

    static long RunOnce(Op op, BigInteger[] a, BigInteger[] b, BigInteger[] e, BigInteger mod, int outer)
    {
      BigInteger acc = BigInteger.Zero;
      int n = a.Length;

      switch (op)
      {
            case Op.ADD_MOD:
                for (int o = 0; o &lt; outer; o++)
                  for (int i = 0; i &lt; n; i++)
                  {
                        var r = (a + b) % mod;
                        acc ^= r;
                  }
                break;

            case Op.MUL_MOD:
                for (int o = 0; o &lt; outer; o++)
                  for (int i = 0; i &lt; n; i++)
                  {
                        var r = (a * b) % mod;
                        acc ^= r;
                  }
                break;

            case Op.MODPOW:
                for (int o = 0; o &lt; outer; o++)
                  for (int i = 0; i &lt; n; i++)
                  {
                        var r = BigInteger.ModPow(a, e, mod);
                        acc ^= r;
                  }
                break;
      }

      return (long)(acc &amp; long.MaxValue); // stable checksum
    }

    static Result Bench(string lang, int bits, Op op, BigInteger[] a, BigInteger[] b, BigInteger[] e, BigInteger mod, long targetOps, int warmups, int measures)
    {
      int n = a.Length;
      int outer = (int)Math.Max(1, targetOps / n);
      long actualOps = (long)n * outer;

      long ck = 0;
      for (int i = 0; i &lt; warmups; i++)
            ck ^= RunOnce(op, a, b, e, mod, outer);

      long[] timesNs = new long;
      for (int i = 0; i &lt; measures; i++)
      {
            var sw = Stopwatch.StartNew();
            long c = RunOnce(op, a, b, e, mod, outer);
            sw.Stop();
            ck ^= c;
            // Stopwatch ticks to ns
            timesNs = (long)(sw.ElapsedTicks * (1_000_000_000.0 / Stopwatch.Frequency));
      }

      Array.Sort(timesNs);
      long median = timesNs;
      double nsPerOp = (double)median / actualOps;
      return new Result(lang, bits, op, actualOps, nsPerOp, ck);
    }

    static void PrintHuman(List&lt;Result&gt; results)
    {
      Console.WriteLine("C# BigInteger benchmark");
      Console.WriteLine($"dotnet.version={Environment.Version}");
      Console.WriteLine($"os={System.Runtime.InteropServices.RuntimeInformation.OSDescription}");
      Console.WriteLine($"arch={System.Runtime.InteropServices.RuntimeInformation.OSArchitecture}");
      Console.WriteLine();
      Console.WriteLine($"{ "Bits",-6} {"Op",-9} {"ns/op(med)",-12} {"ops/run",-12}");
      foreach (var r in results)
            Console.WriteLine(string.Create(CultureInfo.InvariantCulture, $"{r.Bits,-6} {r.Op,-9} {r.NsPerOp,-12:F3} {r.Ops,-12}"));
      Console.WriteLine();
      long checksum = 0;
      foreach (var r in results) checksum ^= r.Checksum;
      Console.WriteLine($"checksum={checksum}");
    }

    public static int Main(string[] args)
    {
      bool json = args.Any(a =&gt; a == "--json");

      int warmups = 5;
      int measures = 11;
      int[] bitSizes = ;

      var results = new List&lt;Result&gt;();

      foreach (int bits in bitSizes)
      {
            var mod = GenModulus(bits, 0xA1B2C3D4E5F60708UL ^ (uint)bits);

            int nAddMul = 1024;
            var a = GenBigInts(bits, nAddMul, 0x1111222233334444UL ^ (uint)bits);
            var b = GenBigInts(bits, nAddMul, 0x9999AAAABBBBCCCCUL ^ (uint)bits);

            int nPow = 256;
            var ap = GenBigInts(bits, nPow, 0x13579BDF2468ACE0UL ^ (uint)bits);
            var ep = GenBigInts(Math.Min(bits, 512), nPow, 0x0FEDCBA987654321UL ^ (uint)bits);

            long addOps = bits switch { 256 =&gt; 2_000_000L, 1024 =&gt; 1_000_000L, _ =&gt; 200_000L };
            long mulOps = bits switch { 256 =&gt; 500_000L, 1024 =&gt; 120_000L, _ =&gt; 20_000L };
            long powOps = bits switch { 256 =&gt; 8_000L, 1024 =&gt; 1_500L, _ =&gt; 250L };

            results.Add(Bench("csharp", bits, Op.ADD_MOD, a, b, null!, mod, addOps, warmups, measures));
            results.Add(Bench("csharp", bits, Op.MUL_MOD, a, b, null!, mod, mulOps, warmups, measures));
            results.Add(Bench("csharp", bits, Op.MODPOW, ap, null!, ep, mod, powOps, warmups, measures));
      }

      if (json)
      {
            foreach (var r in results) Console.WriteLine(r.ToJson());
      }
      else
      {
            PrintHuman(results);
      }

      return 0;
    }
}
</code></pre>
<h3 id="32-运行方式可复现命令">3.2 运行方式(可复现命令)</h3>
<pre><code class="language-bash"># Java 21
cd /app/bigintbench/java
javac BigIntBench.java
java -Xms1g -Xmx1g -XX:+UseG1GC -XX:+AlwaysPreTouch BigIntBench --json

# .NET 10
cd /app/bigintbench/csharp
dotnet build -c Release
DOTNET_GCServer=1 dotnet run -c Release -- --json
</code></pre>
<h3 id="33-实验一原始输出jsonl">3.3 实验一原始输出(JSONL)</h3>
<h4 id="java-21results_java21jsonl">Java 21:<code>results_java21.jsonl</code></h4>
<pre><code class="language-jsonl">{"lang":"java","bits":256,"op":"ADD_MOD","ops":1999872,"nsPerOp":139.589,"checksum":0}
{"lang":"java","bits":256,"op":"MUL_MOD","ops":499712,"nsPerOp":387.031,"checksum":0}
{"lang":"java","bits":256,"op":"MODPOW","ops":7936,"nsPerOp":17764.884,"checksum":0}
{"lang":"java","bits":1024,"op":"ADD_MOD","ops":999424,"nsPerOp":284.672,"checksum":0}
{"lang":"java","bits":1024,"op":"MUL_MOD","ops":119808,"nsPerOp":3094.540,"checksum":0}
{"lang":"java","bits":1024,"op":"MODPOW","ops":1280,"nsPerOp":264577.852,"checksum":0}
{"lang":"java","bits":4096,"op":"ADD_MOD","ops":199680,"nsPerOp":900.735,"checksum":0}
{"lang":"java","bits":4096,"op":"MUL_MOD","ops":19456,"nsPerOp":32062.554,"checksum":0}
{"lang":"java","bits":4096,"op":"MODPOW","ops":256,"nsPerOp":3422756.113,"checksum":0}
</code></pre>
<h4 id="net-10results_csharpjsonl">.NET 10:<code>results_csharp.jsonl</code></h4>
<pre><code class="language-jsonl">{"lang":"csharp","bits":256,"op":"ADD_MOD","ops":1999872,"nsPerOp":146.261,"checksum":0}
{"lang":"csharp","bits":256,"op":"MUL_MOD","ops":499712,"nsPerOp":560.246,"checksum":0}
{"lang":"csharp","bits":256,"op":"MODPOW","ops":7936,"nsPerOp":169713.608,"checksum":0}
{"lang":"csharp","bits":1024,"op":"ADD_MOD","ops":999424,"nsPerOp":297.335,"checksum":0}
{"lang":"csharp","bits":1024,"op":"MUL_MOD","ops":119808,"nsPerOp":4792.760,"checksum":0}
{"lang":"csharp","bits":1024,"op":"MODPOW","ops":1280,"nsPerOp":1938407.720,"checksum":0}
{"lang":"csharp","bits":4096,"op":"ADD_MOD","ops":199680,"nsPerOp":1280.760,"checksum":0}
{"lang":"csharp","bits":4096,"op":"MUL_MOD","ops":19456,"nsPerOp":36894.568,"checksum":0}
{"lang":"csharp","bits":4096,"op":"MODPOW","ops":256,"nsPerOp":20617970.004,"checksum":0}
</code></pre>
<hr>
<h2 id="实验二加入-java-8-看现状主流处于什么位置">实验二:加入 Java 8 ——看“现状主流”处于什么位置</h2>
<p>这一组的关键点是:<strong>同一份 Java 源码用 Java 8 编译</strong>,分别在 Java 8 与 Java 21 上运行,尽量避免“编译器产物差异”。</p>
<h3 id="41-运行方式">4.1 运行方式</h3>
<pre><code class="language-bash"># 编译(Java 8)
/path/to/jdk8/bin/javac BigIntBench.java

# 运行(Java 8)
/path/to/jdk8/bin/java -Xms1g -Xmx1g -XX:+UseG1GC -XX:+AlwaysPreTouch BigIntBench --json

# 运行(Java 21)
java -Xms1g -Xmx1g -XX:+UseG1GC -XX:+AlwaysPreTouch BigIntBench --json
</code></pre>
<h3 id="42-实验二原始输出jsonl">4.2 实验二原始输出(JSONL)</h3>
<h4 id="java-8results_java8jsonl">Java 8:<code>results_java8.jsonl</code></h4>
<pre><code class="language-jsonl">{"lang":"java","bits":256,"op":"ADD_MOD","ops":1999872,"nsPerOp":207.335,"checksum":0}
{"lang":"java","bits":256,"op":"MUL_MOD","ops":499712,"nsPerOp":468.248,"checksum":0}
{"lang":"java","bits":256,"op":"MODPOW","ops":7936,"nsPerOp":17697.113,"checksum":0}
{"lang":"java","bits":1024,"op":"ADD_MOD","ops":999424,"nsPerOp":390.906,"checksum":0}
{"lang":"java","bits":1024,"op":"MUL_MOD","ops":119808,"nsPerOp":3089.474,"checksum":0}
{"lang":"java","bits":1024,"op":"MODPOW","ops":1280,"nsPerOp":277395.652,"checksum":0}
{"lang":"java","bits":4096,"op":"ADD_MOD","ops":199680,"nsPerOp":990.708,"checksum":0}
{"lang":"java","bits":4096,"op":"MUL_MOD","ops":19456,"nsPerOp":30692.214,"checksum":0}
{"lang":"java","bits":4096,"op":"MODPOW","ops":256,"nsPerOp":3468269.539,"checksum":0}
</code></pre>
<hr>
<h2 id="可视化与总结">可视化与总结</h2>
<p>从一个 .NET 程序员的视角来看,这次的测试结果可以说既在情理之中,又有些出乎意料。</p>
<ol>
<li><strong>ADD_MOD (加法+取模)</strong>: 在这个项目上,.NET 和 Java 21 几乎打了个平手,差距微乎其微。可以说,在基础的加法运算上,.NET 表现得相当不错。<br>
<img src="https://img2024.cnblogs.com/blog/233608/202601/233608-20260114225530983-1114904260.png" alt="bar_csharp_baseline_add_mod" loading="lazy"></li>
<li><strong>MUL_MOD (乘法+取模)</strong>: 从这里开始,差距出现了。.NET 明显慢于 Java,性能鸿沟开始变得“肉眼可见”。<br>
<img src="https://img2024.cnblogs.com/blog/233608/202601/233608-20260114225554833-1748112213.png" alt="bar_csharp_baseline_mul_mod" loading="lazy"></li>
<li><strong>MODPOW (模幂)</strong>: 这是差距最大的地方。.NET 在这项测试中被 Java 21 拉开了 <strong>6到9倍</strong> 的差距。对于从事密码学或需要大量大数运算的开发者来说,这是一个非常刺眼的信号。<br>
<img src="https://img2024.cnblogs.com/blog/233608/202601/233608-20260114225547260-463566788.png" alt="bar_csharp_baseline_modpow" loading="lazy"></li>
<li><strong>Java 8 vs Java 21</strong>: 毫无疑问,Java 21 在绝大多数情况下都比老迈的 Java 8 要快。不过有趣的是,在 <code>MUL_MOD</code> 的 1024 和 4096 位测试中,Java 8 居然出现了“反超”的现象。这可能是由于 JIT 策略、算法选择的阈值差异,或是单纯的测量误差。虽然这不影响“Java 21更快”的总体结论,但也提醒我们性能测试的复杂性。</li>
</ol>
<p>总而言之,这次对决让我们清楚地看到,在复杂的大数运算上,.NET 的 <code>BigInteger</code> 确实还有很长的路要走。</p>
<hr>
<h2 id="one-more-thing当外援登场">One More Thing:当“外援”登场</h2>
<p>在寻找 .NET 大数性能优化方案的过程中,我们自然能想到了业界标杆 GMP ——它是 GNU Multi-Precision Arithmetic Library,很多数学软件/密码学实现都会用它做高性能大整数运算。</p>
<p>我碰巧也为它做了一个 .NET 封装:Sdcb.Arithmetic。</p>
<p>但必须提前声明:<strong>让 GMP 作为“外援”加入这场对比,是“不公平”的</strong>。原因很简单:</p>
<ul>
<li><strong>语言优势</strong>:GMP 是原生 C/汇编,而 .NET 和 Java 是在虚拟机上运行的托管语言。</li>
<li><strong>内存策略</strong>:GMP 鼓励使用 <strong>in-place API</strong>,可以直接在原地修改数值,大大减少了内存分配和 GC 压力。而 .NET 和 Java 的 <code>BigInteger</code> 则是不可变对象。</li>
</ul>
<p>所以,这部分的结果更像是一个“彩蛋”,展示的是:<strong>如果你愿意引入原生依赖,并改变编码风格,.NET 的大数性能可以达到怎样的高度。</strong></p>
<h3 id="71-客串实验完整源代码net-biginteger--gmpinteger-同场">7.1 客串实验完整源代码(.NET BigInteger + GmpInteger 同场)</h3>
<blockquote>
<p>下面代码会同时输出两套结果:<code>csharp_bigint</code> 与 <code>csharp_gmp_inplace</code>(仍是 JSONL)。</p>
</blockquote>
<p><strong><code>Program.cs</code>(客串版,完整)</strong></p>
<pre><code class="language-csharp">using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Numerics;
using Sdcb.Arithmetic.Gmp;

sealed class SplitMix64
{
    private ulong _x;
    public SplitMix64(ulong seed) =&gt; _x = seed;

    public ulong NextUInt64()
    {
      ulong z = (_x += 0x9E3779B97F4A7C15UL);
      z = (z ^ (z &gt;&gt; 30)) * 0xBF58476D1CE4E5B9UL;
      z = (z ^ (z &gt;&gt; 27)) * 0x94D049BB133111EBUL;
      return z ^ (z &gt;&gt; 31);
    }

    public void NextBytes(byte[] dst)
    {
      int i = 0;
      while (i &lt; dst.Length)
      {
            ulong v = NextUInt64();
            for (int k = 0; k &lt; 8 &amp;&amp; i &lt; dst.Length; k++)
                dst = (byte)(v &gt;&gt; (56 - 8 * k));
      }
    }
}

enum Op { ADD_MOD, MUL_MOD, MODPOW }

record Result(string Impl, int Bits, Op Op, long Ops, double NsPerOp, long Checksum)
{
    public string ToJson() =&gt; string.Create(CultureInfo.InvariantCulture,
      $"{{\"lang\":\"{Impl}\",\"bits\":{Bits},\"op\":\"{Op}\",\"ops\":{Ops},\"nsPerOp\":{NsPerOp:F3},\"checksum\":{Checksum}}}");
}

static class BenchUtil
{
    public static void MaskToBitSize(byte[] buf, int bitSize)
    {
      int topBit = (bitSize - 1) % 8;
      int keepBits = topBit + 1;
      int firstMask = keepBits == 8 ? 0xFF : ((1 &lt;&lt; keepBits) - 1);
      buf &amp;= (byte)firstMask;
      buf |= (byte)(1 &lt;&lt; topBit);
    }

    public static string ToHex(byte[] bytes) =&gt; Convert.ToHexString(bytes);
}

static class BigIntegerBench
{
    public static BigInteger[] Gen(int bitSize, int count, ulong seed)
    {
      var rng = new SplitMix64(seed);
      int byteLen = (bitSize + 7) / 8;
      var arr = new BigInteger;
      var buf = new byte;

      for (int i = 0; i &lt; count; i++)
      {
            rng.NextBytes(buf);
            BenchUtil.MaskToBitSize(buf, bitSize);
            arr = new BigInteger(buf, isUnsigned: true, isBigEndian: true);
      }
      return arr;
    }

    public static BigInteger GenModulus(int bitSize, ulong seed)
    {
      var rng = new SplitMix64(seed);
      int byteLen = (bitSize + 7) / 8;
      var buf = new byte;
      rng.NextBytes(buf);
      BenchUtil.MaskToBitSize(buf, bitSize);
      buf[^1] |= 1;
      return new BigInteger(buf, isUnsigned: true, isBigEndian: true);
    }

    public static long RunOnce(Op op, BigInteger[] a, BigInteger[] b, BigInteger[] e, BigInteger mod, int outer)
    {
      BigInteger acc = BigInteger.Zero;
      int n = a.Length;

      switch (op)
      {
            case Op.ADD_MOD:
                for (int o = 0; o &lt; outer; o++)
                  for (int i = 0; i &lt; n; i++)
                        acc ^= (a + b) % mod;
                break;

            case Op.MUL_MOD:
                for (int o = 0; o &lt; outer; o++)
                  for (int i = 0; i &lt; n; i++)
                        acc ^= (a * b) % mod;
                break;

            case Op.MODPOW:
                for (int o = 0; o &lt; outer; o++)
                  for (int i = 0; i &lt; n; i++)
                        acc ^= BigInteger.ModPow(a, e, mod);
                break;
      }

      return (long)(acc &amp; long.MaxValue);
    }
}

static class GmpIntegerBench
{
    public static GmpInteger[] Gen(int bitSize, int count, ulong seed)
    {
      var rng = new SplitMix64(seed);
      int byteLen = (bitSize + 7) / 8;
      var arr = new GmpInteger;
      var buf = new byte;

      for (int i = 0; i &lt; count; i++)
      {
            rng.NextBytes(buf);
            BenchUtil.MaskToBitSize(buf, bitSize);
            arr = GmpInteger.Parse(BenchUtil.ToHex(buf), 16);
      }
      return arr;
    }

    public static GmpInteger GenModulus(int bitSize, ulong seed)
    {
      var rng = new SplitMix64(seed);
      int byteLen = (bitSize + 7) / 8;
      var buf = new byte;
      rng.NextBytes(buf);
      BenchUtil.MaskToBitSize(buf, bitSize);
      buf[^1] |= 1;
      return GmpInteger.Parse(BenchUtil.ToHex(buf), 16);
    }

    public static long RunOnce(Op op, GmpInteger[] a, GmpInteger[] b, GmpInteger[] e, GmpInteger mod, int outer)
    {
      using var acc = GmpInteger.From(0);
      using var tmp = GmpInteger.From(0);

      int n = a.Length;
      switch (op)
      {
            case Op.ADD_MOD:
                for (int o = 0; o &lt; outer; o++)
                  for (int i = 0; i &lt; n; i++)
                  {
                        GmpInteger.AddInplace(tmp, a, b);
                        GmpInteger.ModInplace(tmp, tmp, mod);
                        GmpInteger.BitwiseXorInplace(acc, acc, tmp);
                  }
                break;

            case Op.MUL_MOD:
                for (int o = 0; o &lt; outer; o++)
                  for (int i = 0; i &lt; n; i++)
                  {
                        GmpInteger.MultiplyInplace(tmp, a, b);
                        GmpInteger.ModInplace(tmp, tmp, mod);
                        GmpInteger.BitwiseXorInplace(acc, acc, tmp);
                  }
                break;

            case Op.MODPOW:
                for (int o = 0; o &lt; outer; o++)
                  for (int i = 0; i &lt; n; i++)
                  {
                        GmpInteger.PowerModInplace(tmp, a, e, mod);
                        GmpInteger.BitwiseXorInplace(acc, acc, tmp);
                  }
                break;
      }

      return acc.GetHashCode();
    }

    public static void DisposeAll(GmpInteger[] xs)
    {
      foreach (var x in xs) x.Dispose();
    }
}

static class Runner
{
    static Result BenchBigInteger(int bits, Op op, BigInteger[] a, BigInteger[] b, BigInteger[] e, BigInteger mod, long targetOps, int warmups, int measures)
    {
      int n = a.Length;
      int outer = (int)Math.Max(1, targetOps / n);
      long actualOps = (long)n * outer;

      long ck = 0;
      for (int i = 0; i &lt; warmups; i++) ck ^= BigIntegerBench.RunOnce(op, a, b, e, mod, outer);

      long[] timesNs = new long;
      for (int i = 0; i &lt; measures; i++)
      {
            var sw = Stopwatch.StartNew();
            long c = BigIntegerBench.RunOnce(op, a, b, e, mod, outer);
            sw.Stop();
            ck ^= c;
            timesNs = (long)(sw.ElapsedTicks * (1_000_000_000.0 / Stopwatch.Frequency));
      }
      Array.Sort(timesNs);
      long median = timesNs;
      return new Result("csharp_bigint", bits, op, actualOps, (double)median / actualOps, ck);
    }

    static Result BenchGmpInteger(int bits, Op op, GmpInteger[] a, GmpInteger[] b, GmpInteger[] e, GmpInteger mod, long targetOps, int warmups, int measures)
    {
      int n = a.Length;
      int outer = (int)Math.Max(1, targetOps / n);
      long actualOps = (long)n * outer;

      long ck = 0;
      for (int i = 0; i &lt; warmups; i++) ck ^= GmpIntegerBench.RunOnce(op, a, b, e, mod, outer);

      long[] timesNs = new long;
      for (int i = 0; i &lt; measures; i++)
      {
            var sw = Stopwatch.StartNew();
            long c = GmpIntegerBench.RunOnce(op, a, b, e, mod, outer);
            sw.Stop();
            ck ^= c;
            timesNs = (long)(sw.ElapsedTicks * (1_000_000_000.0 / Stopwatch.Frequency));
      }
      Array.Sort(timesNs);
      long median = timesNs;
      return new Result("csharp_gmp_inplace", bits, op, actualOps, (double)median / actualOps, ck);
    }

    public static int Run(string[] args)
    {
      bool json = args.Any(a =&gt; a == "--json");

      int warmups = 5;
      int measures = 11;
      int[] bitSizes = ;

      var results = new List&lt;Result&gt;();

      foreach (int bits in bitSizes)
      {
            long addOps = bits switch { 256 =&gt; 2_000_000L, 1024 =&gt; 1_000_000L, _ =&gt; 200_000L };
            long mulOps = bits switch { 256 =&gt; 500_000L, 1024 =&gt; 120_000L, _ =&gt; 20_000L };
            long powOps = bits switch { 256 =&gt; 8_000L, 1024 =&gt; 1_500L, _ =&gt; 250L };

            // BigInteger data
            var modB = BigIntegerBench.GenModulus(bits, 0xA1B2C3D4E5F60708UL ^ (uint)bits);
            var aB = BigIntegerBench.Gen(bits, 1024, 0x1111222233334444UL ^ (uint)bits);
            var bB = BigIntegerBench.Gen(bits, 1024, 0x9999AAAABBBBCCCCUL ^ (uint)bits);
            var apB = BigIntegerBench.Gen(bits, 256, 0x13579BDF2468ACE0UL ^ (uint)bits);
            var epB = BigIntegerBench.Gen(Math.Min(bits, 512), 256, 0x0FEDCBA987654321UL ^ (uint)bits);

            // GmpInteger data (same seeds/bit sizes)
            using var modG = GmpIntegerBench.GenModulus(bits, 0xA1B2C3D4E5F60708UL ^ (uint)bits);
            var aG = GmpIntegerBench.Gen(bits, 1024, 0x1111222233334444UL ^ (uint)bits);
            var bG = GmpIntegerBench.Gen(bits, 1024, 0x9999AAAABBBBCCCCUL ^ (uint)bits);
            var apG = GmpIntegerBench.Gen(bits, 256, 0x13579BDF2468ACE0UL ^ (uint)bits);
            var epG = GmpIntegerBench.Gen(Math.Min(bits, 512), 256, 0x0FEDCBA987654321UL ^ (uint)bits);

            try
            {
                results.Add(BenchBigInteger(bits, Op.ADD_MOD, aB, bB, null!, modB, addOps, warmups, measures));
                results.Add(BenchBigInteger(bits, Op.MUL_MOD, aB, bB, null!, modB, mulOps, warmups, measures));
                results.Add(BenchBigInteger(bits, Op.MODPOW, apB, null!, epB, modB, powOps, warmups, measures));

                results.Add(BenchGmpInteger(bits, Op.ADD_MOD, aG, bG, null!, modG, addOps, warmups, measures));
                results.Add(BenchGmpInteger(bits, Op.MUL_MOD, aG, bG, null!, modG, mulOps, warmups, measures));
                results.Add(BenchGmpInteger(bits, Op.MODPOW, apG, null!, epG, modG, powOps, warmups, measures));
            }
            finally
            {
                GmpIntegerBench.DisposeAll(aG);
                GmpIntegerBench.DisposeAll(bG);
                GmpIntegerBench.DisposeAll(apG);
                GmpIntegerBench.DisposeAll(epG);
            }
      }

      if (json)
      {
            foreach (var r in results) Console.WriteLine(r.ToJson());
      }

      return 0;
    }
}

public static class Program
{
    public static int Main(string[] args) =&gt; Runner.Run(args);
}
</code></pre>
<p><strong><code>gmpbench.csproj</code>(客串版项目文件)</strong></p>
<pre><code class="language-xml">&lt;Project Sdk="Microsoft.NET.Sdk"&gt;
&lt;PropertyGroup&gt;
    &lt;OutputType&gt;Exe&lt;/OutputType&gt;
    &lt;TargetFramework&gt;net10.0&lt;/TargetFramework&gt;
    &lt;Nullable&gt;enable&lt;/Nullable&gt;
    &lt;ImplicitUsings&gt;enable&lt;/ImplicitUsings&gt;
&lt;/PropertyGroup&gt;

&lt;ItemGroup&gt;
    &lt;PackageReference Include="Sdcb.Arithmetic.Gmp" Version="*" /&gt;
    &lt;PackageReference Include="Sdcb.Arithmetic.Gmp.runtime.linux-x64" Version="*" /&gt;
&lt;/ItemGroup&gt;
&lt;/Project&gt;
</code></pre>
<h3 id="72-客串实验原始输出jsonl">7.2 客串实验原始输出(JSONL)</h3>
<pre><code class="language-jsonl">{"lang":"csharp_bigint","bits":256,"op":"ADD_MOD","ops":1999872,"nsPerOp":146.261,"checksum":0}
{"lang":"csharp_bigint","bits":256,"op":"MUL_MOD","ops":499712,"nsPerOp":560.246,"checksum":0}
{"lang":"csharp_bigint","bits":256,"op":"MODPOW","ops":7936,"nsPerOp":169713.608,"checksum":0}
{"lang":"csharp_gmp_inplace","bits":256,"op":"ADD_MOD","ops":1999872,"nsPerOp":76.644,"checksum":0}
{"lang":"csharp_gmp_inplace","bits":256,"op":"MUL_MOD","ops":499712,"nsPerOp":114.690,"checksum":0}
{"lang":"csharp_gmp_inplace","bits":256,"op":"MODPOW","ops":7936,"nsPerOp":13931.914,"checksum":0}
{"lang":"csharp_bigint","bits":1024,"op":"ADD_MOD","ops":999424,"nsPerOp":297.335,"checksum":0}
{"lang":"csharp_bigint","bits":1024,"op":"MUL_MOD","ops":119808,"nsPerOp":4792.760,"checksum":0}
{"lang":"csharp_bigint","bits":1024,"op":"MODPOW","ops":1280,"nsPerOp":1938407.720,"checksum":0}
{"lang":"csharp_gmp_inplace","bits":1024,"op":"ADD_MOD","ops":999424,"nsPerOp":97.260,"checksum":0}
{"lang":"csharp_gmp_inplace","bits":1024,"op":"MUL_MOD","ops":119808,"nsPerOp":562.235,"checksum":0}
{"lang":"csharp_gmp_inplace","bits":1024,"op":"MODPOW","ops":1280,"nsPerOp":218715.147,"checksum":0}
{"lang":"csharp_bigint","bits":4096,"op":"ADD_MOD","ops":199680,"nsPerOp":1280.760,"checksum":0}
{"lang":"csharp_bigint","bits":4096,"op":"MUL_MOD","ops":19456,"nsPerOp":36894.568,"checksum":0}
{"lang":"csharp_bigint","bits":4096,"op":"MODPOW","ops":256,"nsPerOp":20617970.004,"checksum":0}
{"lang":"csharp_gmp_inplace","bits":4096,"op":"ADD_MOD","ops":199680,"nsPerOp":179.720,"checksum":0}
{"lang":"csharp_gmp_inplace","bits":4096,"op":"MUL_MOD","ops":19456,"nsPerOp":5431.441,"checksum":0}
{"lang":"csharp_gmp_inplace","bits":4096,"op":"MODPOW","ops":256,"nsPerOp":2662198.492,"checksum":0}
</code></pre>
<h3 id="73-客串可视化以gmp为基准">7.3 客串可视化(以GMP为基准)</h3>
<p><img src="https://img2024.cnblogs.com/blog/233608/202601/233608-20260114225617406-1423853952.png" alt="bar_gmp_baseline_add_mod" loading="lazy"><br>
<img src="https://img2024.cnblogs.com/blog/233608/202601/233608-20260114225622152-863625745.png" alt="bar_gmp_baseline_mul_mod" loading="lazy"><br>
<img src="https://img2024.cnblogs.com/blog/233608/202601/233608-20260114225625327-2119127781.png" alt="bar_gmp_baseline_modpow" loading="lazy"></p>
<hr>
<h2 id="总结与展望">总结与展望</h2>
<p>从这次“硬碰硬”的对决中,我们可以清晰地看到:在基础加法上,.NET <code>BigInteger</code> 与 Java 不分伯仲;但在乘法,尤其是<strong>模幂运算</strong>(对密码学等场景极其重要)上,.NET 目前确实存在明显的短板,大幅落后于 Java。</p>
<p>承认不足是改进的开始。对于绝大多数业务场景,内置的 <code>BigInteger</code> 依然够用且方便。但如果你的应用处于性能敏感区(如加密算法、科学计算),那么也许是时候考虑一些“重武器”了。</p>
<p>这也正是我开发并维护 <strong>Sdcb.Arithmetic</strong> 的初衷。它通过封装 GMP 等高性能原生库,为 .NET 带来了<strong>原地修改(in-place)</strong>以及高达数倍的性能提升(如文中实验所示)。如果你对性能有极致追求,或者想看看 .NET 在大数计算上的极限,欢迎去 GitHub 点个 Star ⭐,试一试这个库。</p>
<p>感谢阅读!如果你觉得这两个语言的对比分析有意思,或者对 .NET 高性能编程感兴趣,欢迎在评论区留言交流,也欢迎加入我的 <strong>.NET骚操作 QQ群:495782587</strong>,我们一起探索更多技术硬核玩法。</p><br><br>
来源:https://www.cnblogs.com/sdcb/p/19484525/20261113-big-integer-dotnet-10-vs-java
頁: [1]
查看完整版本: 不服跑个分?.NET 10 大整数计算对阵 Java,结果令人意外